Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 18, 2022
Merged
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: 1 addition & 1 deletion .github/workflows/beta_site_lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Beta Site Lint
name: Beta Site Lint / Heading ID check

on:
pull_request:
Expand Down
3 changes: 1 addition & 2 deletions beta/.husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
. "$(dirname "$0")/_/husky.sh"

cd beta
# yarn generate-ids
# git add -u src/pages/**/*.md
yarn lint-heading-ids
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not fix them instead? Other scripts in this file fix things.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did not want to fix the heading ids since other files without heading are modified while doing translations. Rather we wanted to just throw error so that we dont miss it in the main repo?

yarn prettier
yarn lint:fix
5 changes: 3 additions & 2 deletions beta/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
108 changes: 0 additions & 108 deletions beta/scripts/generateHeadingIDs.js

This file was deleted.

110 changes: 110 additions & 0 deletions beta/scripts/headingIDHelpers/generateHeadingIDs.js
Original file line number Diff line number Diff line change
@@ -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 changes: 68 additions & 0 deletions beta/scripts/headingIDHelpers/validateHeadingIDs.js
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions beta/scripts/headingIDHelpers/walk.js
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions beta/scripts/headingIdLinter.js
Original file line number Diff line number Diff line change
@@ -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);
}