UNPKG

eslint-doc-generator

Version:

Automatic documentation generator for ESLint plugins and rules.

128 lines (127 loc) 6 kB
// General helpers for dealing with markdown files / content. // All markdown content is processed using LF (`\n`) line endings. The desired // end of line for each file is only applied when writing the file. import { END_RULE_HEADER_MARKER, formatComment } from './comment-markers.js'; export function extractFrontmatter(markdown) { const lines = markdown.split('\n'); const frontMatterStart = lines.indexOf('---'); // Frontmatter must start at the beginning of the file to be considered valid, so if we don't find '---' at the beginning, we want to ignore it. if (frontMatterStart !== 0) { return undefined; } const frontMatterEnd = lines.indexOf('---', frontMatterStart + 1); if (frontMatterEnd !== -1) { return lines.slice(frontMatterStart, frontMatterEnd + 1).join('\n'); } return undefined; } /** * Replace the frontmatter, if present. If not and we have newFrontmatter to add, then add it at the beginning. * @param markdown - doc content * @param newFrontmatter - new frontmatter */ export function replaceOrCreateFrontmatter(markdown, newFrontmatter) { // If we don't have any frontmatter coming in, then just return the original markdown if (!newFrontmatter) { return markdown; } const lines = markdown.split('\n'); const frontmatterStartIndex = lines.indexOf('---'); // If there's no existing frontmatter, just add it to the top. if (frontmatterStartIndex !== 0) { return [newFrontmatter, markdown].join('\n'); } const frontmatterEndIndex = lines.indexOf('---', frontmatterStartIndex + 1); const postFrontmatter = lines.slice(frontmatterEndIndex + 1).join('\n'); return [newFrontmatter, postFrontmatter].join('\n'); } /** * Replace the header of a doc up to and including the specified marker. * Insert at beginning if header doesn't exist. * @param markdown - doc content * @param newHeader - new header including marker * @param isMdx - is the file we're working on mdx or just regular md */ export function replaceOrCreateHeader(markdown, newHeader, isMdx) { const lines = markdown.split('\n'); const titleLineIndex = lines.findIndex((line) => line.startsWith('# ')); const markerLineIndex = lines.indexOf(formatComment(END_RULE_HEADER_MARKER, isMdx)); const dashesLineIndex1 = lines.indexOf('---'); const dashesLineIndex2 = lines.indexOf('---', dashesLineIndex1 + 1); // Any YAML front matter or anything else above the title should be kept as-is ahead of the new header. const preHeader = lines .slice(0, Math.max(titleLineIndex, dashesLineIndex2 + 1)) .join('\n'); // Anything after the marker comment, title, or YAML front matter should be kept as-is after the new header. const postHeader = lines .slice(Math.max(markerLineIndex + 1, titleLineIndex + 1, dashesLineIndex2 + 1)) .join('\n'); return `${preHeader ? `${preHeader}\n` : ''}${newHeader}\n${postHeader}`; } /** * Find the section most likely to be the top-level section for a given string. */ export function findSectionHeader(markdown, str) { // Get all the matching strings. const regexp = new RegExp(`## .*${str}.*\n`, 'giu'); const sectionPotentialMatches = [...markdown.matchAll(regexp)].map((match) => match[0]); if (sectionPotentialMatches.length === 0) { // No section found. return undefined; } if (sectionPotentialMatches.length === 1) { // If there's only one match, we can assume it's the section. return sectionPotentialMatches[0]; } // Otherwise assume the shortest match is the correct one. return sectionPotentialMatches.toSorted((a, b) => a.length - b.length)[0]; } export function findFinalHeaderLevel(context, str) { const { options: { framework }, } = context; const lines = str.split('\n'); const finalHeader = lines .toReversed() .find((line) => line.match('^(#+) .+$')); if (finalHeader) { return finalHeader.indexOf(' '); } // If the framework is `starlight` and there's frontmatter at the top, treat that as an H1 else if (framework === 'starlight' && extractFrontmatter(str)) { return 1; } return undefined; } /** * Ensure a doc contains (or doesn't contain) some particular content. * Upon failure, output the failure and set a failure exit code. * @param docName - name of doc for error message * @param contentName - name of content for error message * @param contents - the doc's contents * @param content - the content we are checking for * @param expected - whether the content should be present or not present */ export function expectContentOrFail(docName, contentName, contents, content, expected) { // Check for the content and also the versions of the content with escaped quotes // in case escaping is needed where the content is referenced. const hasContent = contents.includes(content) || contents.includes(content.replaceAll('"', String.raw `\"`)) || contents.includes(content.replaceAll("'", String.raw `\'`)); if (hasContent !== expected) { console.error(`${docName} should ${ /* istanbul ignore next -- TODO: test !expected or remove parameter */ expected ? '' : 'not '}have included ${contentName}: ${content}`); process.exitCode = 1; } } export function expectSectionHeaderOrFail(contentName, contents, possibleHeaders, expected) { const found = possibleHeaders.some((header) => findSectionHeader(contents, header)); if (found !== expected) { if (possibleHeaders.length > 1) { console.error(`${contentName} should ${expected ? '' : 'not '}have included ${expected ? 'one' : 'any'} of these headers: ${possibleHeaders.join(', ')}`); } else { console.error(`${contentName} should ${expected ? '' : 'not '}have included the header: ${possibleHeaders.join(', ')}`); } process.exitCode = 1; } }