Browse Source

Added hook to validate if headings are present or not (#4143)

* Added hook to validate if headings are present or not

* Remove un wanted default param

* Add validate Ids to ci check too

* Revamp heading id generation and validation workflow

* Update validateHeadingIDs.js
main
Strek 3 years ago
committed by GitHub
parent
commit
0b21acb5ab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/beta_site_lint.yml
  2. 3
      beta/.husky/pre-commit
  3. 5
      beta/package.json
  4. 108
      beta/scripts/generateHeadingIDs.js
  5. 110
      beta/scripts/headingIDHelpers/generateHeadingIDs.js
  6. 68
      beta/scripts/headingIDHelpers/validateHeadingIDs.js
  7. 24
      beta/scripts/headingIDHelpers/walk.js
  8. 16
      beta/scripts/headingIdLinter.js

2
.github/workflows/beta_site_lint.yml

@ -1,4 +1,4 @@
name: Beta Site Lint
name: Beta Site Lint / Heading ID check
on:
pull_request:

3
beta/.husky/pre-commit

@ -2,7 +2,6 @@
. "$(dirname "$0")/_/husky.sh"
cd beta
# yarn generate-ids
# git add -u src/pages/**/*.md
yarn lint-heading-ids
yarn prettier
yarn lint:fix

5
beta/package.json

@ -13,8 +13,9 @@
"nit:source": "prettier --config .prettierrc --list-different \"{plugins,src}/**/*.{js,ts,jsx,tsx}\"",
"prettier": "yarn format:source",
"prettier:diff": "yarn nit:source",
"generate-ids": "node scripts/generateHeadingIDs.js src/pages/",
"ci-check": "npm-run-all prettier:diff --parallel lint tsc",
"lint-heading-ids":"node scripts/headingIdLinter.js",
"fix-headings": "node scripts/headingIdLinter.js --fix",
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids",
"tsc": "tsc --noEmit",
"start": "next start",
"postinstall": "is-ci || (cd .. && husky install beta/.husky)",

108
beta/scripts/generateHeadingIDs.js

@ -1,108 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*/
// To do: Make this ESM.
// To do: properly check heading numbers (headings with the same text get
// numbered, this script doesn’t check that).
const assert = require('assert');
const fs = require('fs');
const GithubSlugger = require('github-slugger');
let modules
function walk(dir) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + '/' + file;
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(walk(file));
} else {
/* Is a file */
results.push(file);
}
});
return results;
}
function stripLinks(line) {
return line.replace(/\[([^\]]+)\]\([^)]+\)/, (match, p1) => p1);
}
function addHeaderID(line, slugger) {
// check if we're a header at all
if (!line.startsWith('#')) {
return line;
}
const match = /^(#+\s+)(.+?)(\s*\{(?:\/\*|#)([^\}\*\/]+)(?:\*\/)?\}\s*)?$/.exec(line);
const before = match[1] + match[2]
const proc = modules.unified().use(modules.remarkParse).use(modules.remarkSlug)
const tree = proc.runSync(proc.parse(before))
const head = tree.children[0]
assert(head && head.type === 'heading', 'expected `' + before + '` to be a heading, is it using a normal space after `#`?')
const autoId = head.data.id
const existingId = match[4]
const id = existingId || autoId
// Ignore numbers:
const cleanExisting = existingId ? existingId.replace(/-\d+$/, '') : undefined
const cleanAuto = autoId.replace(/-\d+$/, '')
if (cleanExisting && cleanExisting !== cleanAuto) {
console.log('Note: heading `%s` has a different ID (`%s`) than what GH generates for it: `%s`:', before, existingId, autoId)
}
return match[1] + match[2] + ' {/*' + id + '*/}';
}
function addHeaderIDs(lines) {
// Sluggers should be per file
const slugger = new GithubSlugger();
let inCode = false;
const results = [];
lines.forEach((line) => {
// Ignore code blocks
if (line.startsWith('```')) {
inCode = !inCode;
results.push(line);
return;
}
if (inCode) {
results.push(line);
return;
}
results.push(addHeaderID(line, slugger));
});
return results;
}
const [path] = process.argv.slice(2);
main()
async function main() {
const [unifiedMod, remarkParseMod, remarkSlugMod] = await Promise.all([import('unified'), import('remark-parse'), import('remark-slug')])
const unified = unifiedMod.default
const remarkParse = remarkParseMod.default
const remarkSlug = remarkSlugMod.default
modules = {unified, remarkParse, remarkSlug}
const files = walk(path);
files.forEach((file) => {
if (!(file.endsWith('.md') || file.endsWith('.mdx'))) {
return;
}
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const updatedLines = addHeaderIDs(lines);
fs.writeFileSync(file, updatedLines.join('\n'));
});
}

110
beta/scripts/headingIDHelpers/generateHeadingIDs.js

@ -0,0 +1,110 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*/
// To do: Make this ESM.
// To do: properly check heading numbers (headings with the same text get
// numbered, this script doesn’t check that).
const assert = require('assert');
const fs = require('fs');
const GithubSlugger = require('github-slugger');
const walk = require('./walk');
let modules;
function stripLinks(line) {
return line.replace(/\[([^\]]+)\]\([^)]+\)/, (match, p1) => p1);
}
function addHeaderID(line, slugger) {
// check if we're a header at all
if (!line.startsWith('#')) {
return line;
}
const match =
/^(#+\s+)(.+?)(\s*\{(?:\/\*|#)([^\}\*\/]+)(?:\*\/)?\}\s*)?$/.exec(line);
const before = match[1] + match[2];
const proc = modules
.unified()
.use(modules.remarkParse)
.use(modules.remarkSlug);
const tree = proc.runSync(proc.parse(before));
const head = tree.children[0];
assert(
head && head.type === 'heading',
'expected `' +
before +
'` to be a heading, is it using a normal space after `#`?'
);
const autoId = head.data.id;
const existingId = match[4];
const id = existingId || autoId;
// Ignore numbers:
const cleanExisting = existingId
? existingId.replace(/-\d+$/, '')
: undefined;
const cleanAuto = autoId.replace(/-\d+$/, '');
if (cleanExisting && cleanExisting !== cleanAuto) {
console.log(
'Note: heading `%s` has a different ID (`%s`) than what GH generates for it: `%s`:',
before,
existingId,
autoId
);
}
return match[1] + match[2] + ' {/*' + id + '*/}';
}
function addHeaderIDs(lines) {
// Sluggers should be per file
const slugger = new GithubSlugger();
let inCode = false;
const results = [];
lines.forEach((line) => {
// Ignore code blocks
if (line.startsWith('```')) {
inCode = !inCode;
results.push(line);
return;
}
if (inCode) {
results.push(line);
return;
}
results.push(addHeaderID(line, slugger));
});
return results;
}
async function main(paths) {
paths = paths.length === 0 ? ['src/pages'] : paths;
const [unifiedMod, remarkParseMod, remarkSlugMod] = await Promise.all([
import('unified'),
import('remark-parse'),
import('remark-slug'),
]);
const unified = unifiedMod.default;
const remarkParse = remarkParseMod.default;
const remarkSlug = remarkSlugMod.default;
modules = {unified, remarkParse, remarkSlug};
const files = paths.map((path) => [...walk(path)]).flat();
files.forEach((file) => {
if (!(file.endsWith('.md') || file.endsWith('.mdx'))) {
return;
}
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
const updatedLines = addHeaderIDs(lines);
fs.writeFileSync(file, updatedLines.join('\n'));
});
}
module.exports = main;

68
beta/scripts/headingIDHelpers/validateHeadingIDs.js

@ -0,0 +1,68 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*/
const fs = require('fs');
const walk = require('./walk');
/**
* Validate if there is a custom heading id and exit if there isn't a heading
* @param {string} line
* @returns
*/
function validateHeaderId(line) {
if (!line.startsWith('#')) {
return;
}
const match = /\{\/\*(.*?)\*\/}/.exec(line);
const id = match;
if (!id) {
console.error(
'Run yarn fix-headings to generate headings.'
);
process.exit(1);
}
}
/**
* Loops through the lines to skip code blocks
* @param {Array<string>} lines
*/
function validateHeaderIds(lines) {
let inCode = false;
const results = [];
lines.forEach((line) => {
// Ignore code blocks
if (line.startsWith('```')) {
inCode = !inCode;
results.push(line);
return;
}
if (inCode) {
results.push(line);
return;
}
validateHeaderId(line);
});
}
/**
* paths are basically array of path for which we have to validate heading IDs
* @param {Array<string>} paths
*/
async function main(paths) {
paths = paths.length === 0 ? ['src/pages'] : paths;
const files = paths.map((path) => [...walk(path)]).flat();
files.forEach((file) => {
if (!(file.endsWith('.md') || file.endsWith('.mdx'))) {
return;
}
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
validateHeaderIds(lines);
});
}
module.exports = main;

24
beta/scripts/headingIDHelpers/walk.js

@ -0,0 +1,24 @@
const fs = require('fs');
module.exports = function walk(dir) {
let results = [];
/**
* If the param is a directory we can return the file
*/
if(dir.includes('md')){
return [dir];
}
const list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + '/' + file;
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(walk(file));
} else {
/* Is a file */
results.push(file);
}
});
return results;
};

16
beta/scripts/headingIdLinter.js

@ -0,0 +1,16 @@
const validateHeaderIds = require('./headingIDHelpers/validateHeadingIDs');
const generateHeadingIds = require('./headingIDHelpers/generateHeadingIDs');
/**
* yarn lint-heading-ids --> Checks all files and causes an error if heading ID is missing
* yarn lint-heading-ids --fix --> Fixes all markdown file's heading IDs
* yarn lint-heading-ids path/to/markdown.md --> Checks that particular file for missing heading ID (path can denote a directory or particular file)
* yarn lint-heading-ids --fix path/to/markdown.md --> Fixes that particular file's markdown IDs (path can denote a directory or particular file)
*/
const markdownPaths = process.argv.slice(2);
if (markdownPaths.includes('--fix')) {
generateHeadingIds(markdownPaths.filter((path) => path !== '--fix'));
} else {
validateHeaderIds(markdownPaths);
}
Loading…
Cancel
Save