Browse Source
* 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.jsmain
Strek
3 years ago
committed by
GitHub
8 changed files with 223 additions and 113 deletions
@ -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')); |
|||
}); |
|||
|
|||
} |
@ -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; |
@ -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; |
@ -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; |
|||
}; |
@ -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…
Reference in new issue