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
2 changes: 2 additions & 0 deletions .github/workflows/auto-compress-images.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Compress Images Once a Month - Creates Pull Request
env:
HUSKY: 0

on:
schedule:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/call-algolia-deployment-script.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# .github/workflows/call-algolia-deployment-script.yml
name: Call algolia deployment script on merge
env:
HUSKY: 0

on:
push:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/check-a11y-of-changed-content.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Check accessibility of changed content
env:
HUSKY: 0

on:
pull_request:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/generate-related.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Generate Read More related

env:
HUSKY: 0

on:
workflow_dispatch:
branches:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Lint Posts
env:
HUSKY: 0

on:
pull_request:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/remove-unused-images.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Remove Unused Images
env:
HUSKY: 0

on:
workflow_dispatch:
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm run lint-commit
37 changes: 37 additions & 0 deletions lintCommit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { execSync } = require("child_process");
const { logError, lintPost, getValidCategories } = require("./lintHelper");

const LINTER_MATCH_PATTERN = /^_posts.*\.(md|markdown|html)$/;

const lintCommit = () => {
const categories = getValidCategories();

const changedFiles = execSync("git diff --cached --name-only", {
encoding: "utf-8",
})
.split("\n")
.filter((file) => file.match(LINTER_MATCH_PATTERN));

if (changedFiles.length === 0) {
console.log("No relevant post files changed.");
process.exit(0);
}

console.log("Linting posts to be committed:", changedFiles);

let fail = false;

for (const file of changedFiles) {
if (!lintPost(file, categories))
{
fail = true;
}
}

if (fail) {
logError("Commit blocked due to linting errors.");
process.exit(1);
}
}

lintCommit()
100 changes: 100 additions & 0 deletions lintHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const matter = require("gray-matter");
const yaml = require("js-yaml");
const fs = require("fs");
const clc = require("cli-color");

const MAX_CATEGORIES = 3;

const errorColour = clc.red.bold;
const warningColour = clc.yellow;

const logError = (...params) =>
console.error(errorColour(...params));

const logWarning = (...params) =>
console.warn(warningColour(...params));

const flatMap = (arr, mapFunc) =>
arr.reduce((prev, x) => prev.concat(mapFunc(x)), []);

const getValidCategories = () => {
const categoriesYaml = yaml.safeLoad(
fs.readFileSync("_data/categories.yml", "utf8")
);

const categories = flatMap(
// remove 'Latest Articles' which is a pseudo-category
categoriesYaml.filter(c => c.url.startsWith("/category/")),
// merge category title into sub-categories
c => [c.title].concat(c.subcategories ? c.subcategories : [])
).map(c => c.toLowerCase());

console.log("Valid categories are: " + categories.join(', '));

return categories;
};

const lintPost = (path, categories) => {
try {
const blogPost = fs.readFileSync(path, "utf8");
const frontMatter = matter(blogPost);
const frontMatterCats = frontMatter.data.categories;

let category;
let postCategories;
// if the frontmatter defines a 'category' field:
if (frontMatter.data.category) {
category = frontMatter.data.category.toLowerCase();
postCategories = [category];
// if the frontmatter defines a 'categories' field with at least one but no more than 3 values:

} else if (frontMatterCats && frontMatterCats.length && frontMatterCats.length <= MAX_CATEGORIES) {
postCategories = frontMatter.data.categories.map(c => c.toLowerCase());
category = postCategories[0];
} else {
logError("The post " + path + " does not have at least one and no more than " + MAX_CATEGORIES + " categories defined.");
return false;
}

if (!categories.includes(category)) {
logError(
"The post " + path + " does not have a recognised category"
);
return false;
} else {
postCategories
.filter(c => !categories.includes(c))
.forEach(c => logWarning(
"The post " + path + " has an unrecognised category: '" + c + "'. Check spelling or remove/move to tags."
));
}

const summary = frontMatter.data.summary;
const pathArray = path.split("/");
const postDateString = pathArray[pathArray.length - 1].substring(0, 10);
const postDate = new Date(postDateString);
if (postDate > new Date("2018-03-26")) {
// Note _prose.yml specifies 130 characters are needed, so if you change this please also change the instructions
if(!summary) {
logError("The post " + path + " does not have a summary.")
return false;
}
else if (summary.length < 130) {
logWarning(
"The post " + path + " summary length is " + summary.length + ". Recommended minimum length for the summary is 130 characters."
);
}
}
} catch (e) {
logError(path, e);
return false;
}
return true;
}

module.exports = {
logError,
logWarning,
getValidCategories,
lintPost
}
86 changes: 5 additions & 81 deletions lintPosts.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
const globby = require("globby");
const matter = require("gray-matter");
const yaml = require("js-yaml");
const fs = require("fs");
const clc = require("cli-color");
const LINTER_MATCH_PATTERN="_posts/**/*.{md,markdown,html}";
const MAX_CATEGORIES = 3;

const errorColour = clc.red.bold;
const warningColour = clc.yellow;

const logError = (...params) =>
console.error(errorColour(...params));
const { logError, getValidCategories, lintPost } = require("./lintHelper");

const logWarning = (...params) =>
console.warn(warningColour(...params));

const flatMap = (arr, mapFunc) =>
arr.reduce((prev, x) => prev.concat(mapFunc(x)), []);
const LINTER_MATCH_PATTERN="_posts/**/*.{md,markdown,html}";

const lintAuthorsYml = () => {
const authorsPath = "_data/authors.yml";
Expand Down Expand Up @@ -55,78 +42,15 @@ const lintAuthorsYml = () => {
};

const lintPosts = () => {
const categoriesYaml = yaml.safeLoad(
fs.readFileSync("_data/categories.yml", "utf8")
);

const categories = flatMap(
// remove 'Latest Articles' which is a pseudo-category
categoriesYaml.filter(c => c.url.startsWith("/category/")),
// merge category title into sub-categories
c => [c.title].concat(c.subcategories ? c.subcategories : [])
).map(c => c.toLowerCase());

console.log("Valid categories are: " + categories.join(', '));
const categories = getValidCategories();

let fail = false;

// lint each blog post
globby([LINTER_MATCH_PATTERN]).then(paths => {
paths.forEach(path => {
try {
const blogPost = fs.readFileSync(path, "utf8");
const frontMatter = matter(blogPost);
const frontMatterCats = frontMatter.data.categories;

let category;
let postCategories;
// if the frontmatter defines a 'category' field:
if (frontMatter.data.category) {
category = frontMatter.data.category.toLowerCase();
postCategories = [category];
// if the frontmatter defines a 'categories' field with at least one but no more than 3 values:

} else if (frontMatterCats && frontMatterCats.length && frontMatterCats.length <= MAX_CATEGORIES) {
postCategories = frontMatter.data.categories.map(c => c.toLowerCase());
category = postCategories[0];
} else {
logError("The post " + path + " does not have at least one and no more than " + MAX_CATEGORIES + " categories defined.");
fail = true;
return;
}

if (!categories.includes(category)) {
logError(
"The post " + path + " does not have a recognised category"
);
fail = true;
} else {
postCategories
.filter(c => !categories.includes(c))
.forEach(c => logWarning(
"The post " + path + " has an unrecognised category: '" + c + "'. Check spelling or remove/move to tags."
));
}


const summary = frontMatter.data.summary;
const pathArray = path.split("/");
const postDateString = pathArray[pathArray.length - 1].substring(0, 10);
const postDate = new Date(postDateString);
if (postDate > new Date("2018-03-26")) {
// Note _prose.yml specifies 130 characters are needed, so if you change this please also change the instructions
if(!summary) {
logError("The post " + path + " does not have a summary.")
fail = true;
}
else if (summary.length < 130) {
logWarning(
"The post " + path + " summary length is " + summary.length + ". Recommended minimum length for the summary is 130 characters."
);
}
}
} catch (e) {
logError(path, e);
if (!lintPost(path, categories))
{
fail = true;
}
});
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"glob-promise": "^4.2.2",
"globby": "^7.1.1",
"gray-matter": "^3.1.1",
"husky": "^9.1.7",
"js-yaml": "^3.10.0",
"markdown-spellcheck": "^1.3.1",
"markdown-to-txt": "^2.0.0",
Expand All @@ -32,12 +33,14 @@
},
"scripts": {
"lint": "node lintPosts.js",
"lint-commit": "node lintCommit.js",
"compute-embeddings": "node scripts/generate-related/compute-embeddings.js",
"generate-related": "node scripts/generate-related/blog-metadata.js",
"remove-unused-images": "node scripts/images/remove-images.js",
"spellcheck": "mdspell \"**/ceberhardt/_posts/*.md\" --en-gb -a -n -x -t",
"style": "sass --no-source-map --style=compressed scss/style.scss style.css",
"scripts": "uglifyjs scripts/initialise-menu.js scripts/jquery-1.9.1.js scripts/jquery.jscroll-2.2.4.js scripts/load-clap-count.js scripts/elapsed.js scripts/graft-studio/header-scroll.js scripts/graft-studio/jquery.mmenu.all.js scripts/graft-studio/jquery.matchHeight.js node_modules/applause-button/dist/applause-button.js node_modules/cookieconsent/build/cookieconsent.min.js -o script.js"
"scripts": "uglifyjs scripts/initialise-menu.js scripts/jquery-1.9.1.js scripts/jquery.jscroll-2.2.4.js scripts/load-clap-count.js scripts/elapsed.js scripts/graft-studio/header-scroll.js scripts/graft-studio/jquery.mmenu.all.js scripts/graft-studio/jquery.matchHeight.js node_modules/applause-button/dist/applause-button.js node_modules/cookieconsent/build/cookieconsent.min.js -o script.js",
"prepare": "husky || true"
},
"homepage": "http://blog.scottlogic.com",
"private": true
Expand Down