eslint-doc-generator
Version:
Automatic documentation generator for ESLint plugins and rules.
81 lines (80 loc) • 2.94 kB
JavaScript
import { EOL } from 'node:os';
import { resolve } from 'node:path';
import editorconfig from 'editorconfig';
/**
* Create a memoized end-of-line resolver scoped to one `generate()` run.
* Cache keys are absolute file paths so sibling `.md`/`.mdx` files can differ.
*
* Write-time precedence for `resolve()`:
* 1. Explicit EditorConfig `end_of_line`
* 2. Predominant end of line in existing contents (skip when `contents` is undefined)
* 3. `os.EOL`
*
* Prettier config is not read — run Prettier via `postprocess` if needed.
*/
export function createEndOfLineResolver() {
const explicitCache = new Map();
/** Shared across files so EditorConfig does not re-read the same files. */
const editorConfigFileCache = new Map();
async function getExplicitEndOfLine(filePath) {
const absolutePath = resolve(filePath);
let cached = explicitCache.get(absolutePath);
if (!cached) {
cached = getEndOfLineFromEditorConfig(absolutePath, editorConfigFileCache);
explicitCache.set(absolutePath, cached);
}
const result = await cached;
return result;
}
async function resolveFileEndOfLine(filePath, contents) {
return ((await getExplicitEndOfLine(filePath)) ??
(contents === undefined ? undefined : detectEndOfLine(contents)) ??
getFallbackEndOfLine());
}
return {
getExplicitEndOfLine,
resolve: resolveFileEndOfLine,
};
}
async function getEndOfLineFromEditorConfig(filePath, cache) {
const editorConfigProps = await editorconfig.parse(filePath, { cache });
if (editorConfigProps.end_of_line === 'lf') {
return '\n';
}
if (editorConfigProps.end_of_line === 'crlf') {
return '\r\n';
}
return undefined;
}
/**
* Detect the predominant end of line in the given file contents.
* Returns undefined if the contents have no line breaks.
*/
export function detectEndOfLine(contents) {
const crlfCount = contents.split('\r\n').length - 1;
// All LFs minus those that are part of a CRLF.
const lfCount = contents.split('\n').length - 1 - crlfCount;
if (crlfCount === 0 && lfCount === 0) {
return undefined;
}
// A tie favors LF.
return crlfCount > lfCount ? '\r\n' : '\n';
}
/**
* Convert all line endings in the given contents to the given end of line.
*/
export function normalizeEndOfLine(contents, endOfLine) {
return contents.replaceAll(/\r\n|[\r\n]/gu, endOfLine);
}
/** Fallback when there is no config and no detectable end of line. */
export function getFallbackEndOfLine() {
return getNodeEOL();
}
/* istanbul ignore next */
/** `EOL` is typed as `string`, so we perform run-time validation to be safe. */
function getNodeEOL() {
if (EOL === '\n' || EOL === '\r\n') {
return EOL;
}
throw new Error(`Failed to detect the end-of-line constant from the JavaScript runtime: ${EOL}`);
}