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