cspell
Version:
A Spelling Checker for Code!
1,033 lines (1,000 loc) • 47.8 kB
JavaScript
import { ApplicationError, CheckFailed, DEFAULT_CACHE_LOCATION, IncludeExcludeFlag, ReportChoicesAll, checkText, console, createInit, lint, listDictionaries, npmPackage, parseApplicationFeatureFlags, suggestions, trace } from "./application-_MFvh02K.js";
import { Option, program } from "commander";
import { satisfies } from "semver";
import chalk from "chalk";
import { Link } from "cspell-lib";
import assert from "node:assert";
import { stripVTControlCharacters } from "node:util";
import * as iPath from "node:path";
//#region src/commandCheck.ts
function commandCheck(prog) {
return prog.command("check <files...>").description("Spell check file(s) and display the result. The full file is displayed in color.").option("-c, --config <cspell.json>", "Configuration file to use. By default cspell looks for cspell.json in the current directory.").option("--validate-directives", "Validate in-document CSpell directives.").option("--no-validate-directives", "Do not validate in-document CSpell directives.").option("--no-color", "Turn off color.").option("--color", "Force color").option("--no-exit-code", "Do not return an exit code if issues are found.").addOption(new Option("--default-configuration", "Load the default configuration and dictionaries.").hideHelp()).addOption(new Option("--no-default-configuration", "Do not load the default configuration and dictionaries.")).action(async (files, options) => {
const useExitCode = options.exitCode ?? true;
parseApplicationFeatureFlags(options.flag);
let issueCount = 0;
for (const filename of files) {
console.log(chalk.yellowBright(`Check file: ${filename}`));
console.log();
try {
const result = await checkText(filename, options);
for (const item of result.items) {
const fn = item.flagIE === IncludeExcludeFlag.EXCLUDE ? chalk.gray : item.isError ? chalk.red : chalk.whiteBright;
const t = fn(item.text);
process.stdout.write(t);
issueCount += item.isError ? 1 : 0;
}
console.log();
} catch {
console.error(`File not found "${filename}"`);
throw new CheckFailed("File not found", 1);
}
console.log();
}
if (issueCount) {
const exitCode = useExitCode ?? true ? 1 : 0;
throw new CheckFailed("Issues found", exitCode);
}
});
}
//#endregion
//#region src/commandHelpers.ts
/**
* Collects string values into an array.
* @param value the new value(s) to collect.
* @param previous the previous values.
* @returns the new values appended to the previous values.
*/
function collect(value, previous) {
const values = Array.isArray(value) ? value : [value];
return previous ? [...previous, ...values] : values;
}
/**
* Create Option - a helper function to create a commander option.
* @param name - the name of the option
* @param description - the description of the option
* @param parseArg - optional function to parse the argument
* @param defaultValue - optional default value
* @returns CommanderOption
*/
function crOpt(name, description, parseArg, defaultValue) {
const option = new Option(name, description);
if (parseArg) option.argParser(parseArg);
if (defaultValue !== void 0) option.default(defaultValue);
return option;
}
//#endregion
//#region src/util/pad.ts
function pad(s, w) {
const p = padWidth(s, w);
if (!p) return s;
return s.padEnd(p + s.length);
}
function padWidth(s, target) {
const sWidth = ansiWidth(s);
return Math.max(target - sWidth, 0);
}
function padLeft(s, w) {
const p = padWidth(s, w);
if (!p) return s;
return s.padStart(p + s.length);
}
function isAnsiString(s) {
return s.includes("\x1B") || s.includes("");
}
function width(s) {
assert(!s.includes("\x1B"), "String contains ANSI control characters");
return s.replaceAll(/[\u0000-\u001F\u0300-\u036F]/g, "").replaceAll(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, ".").replaceAll("…", ".").replaceAll(/\p{M}/gu, "").length;
}
function ansiWidth(s) {
return width(stripVTControlCharacters(s));
}
/**
* Prune the end of a string to fit within a specified width, adding an ellipsis if necessary.
* @param str - the text to prune - ANSI is not supported
* @param maxWidth - the maximum width of the text
* @param pad - the string to use for padding, default is '…'
* @returns the pruned text
*/
function pruneTextEnd(str, maxWidth$2, pad$1 = "…") {
if (!maxWidth$2 || maxWidth$2 <= 0) return str;
if (str.length <= maxWidth$2) return str;
if (isAnsiString(str)) return pruneAnsiTextEnd(str, maxWidth$2, pad$1);
const padWidth$1 = width(pad$1);
const maxWidthWithPad = maxWidth$2 - padWidth$1;
const letters = [...str];
let len = 0;
for (let i = 0; i < letters.length; i++) {
const c = letters[i];
len += width(c);
if (len > maxWidthWithPad) {
let j = i + 1;
while (j < letters.length && width(letters[j]) === 0) ++j;
return j === letters.length ? str : letters.slice(0, i).join("") + pad$1;
}
}
return str;
}
/**
* Prune the start of a string to fit within a specified width, adding an ellipsis if necessary.
* @param str - the text to prune - ANSI is not supported
* @param maxWidth - the maximum width of the text
* @param pad - the string to use for padding, default is '…'
* @returns the pruned text
*/
function pruneTextStart(str, maxWidth$2, pad$1 = "…") {
if (!maxWidth$2 || maxWidth$2 <= 0) return str;
if (str.length <= maxWidth$2) return str;
const padWidth$1 = width(pad$1);
const maxWidthWithPad = maxWidth$2 - padWidth$1;
const letters = [...str];
let len = 0;
for (let i = letters.length - 1; i >= 1; i--) {
const c = letters[i];
len += width(c);
if (len > maxWidthWithPad) {
i += 1;
while (i < letters.length && width(letters[i]) === 0) ++i;
return pad$1 + letters.slice(i).join("");
}
}
return str;
}
const ansi = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/\\#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", "g");
function parseAnsiStr(str) {
const fragments = [];
let lastIndex = 0;
for (const match of str.matchAll(ansi)) {
if (match.index > lastIndex) fragments.push({
type: "text",
text: str.slice(lastIndex, match.index)
});
fragments.push({
type: "ansi",
text: match[0]
});
lastIndex = match.index + match[0].length;
}
if (lastIndex < str.length) fragments.push({
type: "text",
text: str.slice(lastIndex)
});
return fragments;
}
/**
* Prune the end of a string to fit within a specified width, adding an ellipsis if necessary.
* @param str - the text to prune - ANSI is supported
* @param maxWidth - the maximum width of the text
* @param pad - the string to use for padding, default is '…'
* @returns the pruned text
*/
function pruneAnsiTextEnd(str, maxWidth$2, pad$1 = "…") {
if (!maxWidth$2 || maxWidth$2 <= 0) return str;
if (str.length <= maxWidth$2) return str;
if (ansiWidth(str) <= maxWidth$2) return str;
const padWidth$1 = ansiWidth(pad$1);
const fragments = parseAnsiStr(str);
const maxWidthWithPad = maxWidth$2 - padWidth$1;
let remaining = maxWidthWithPad;
for (const frag of fragments) {
if (frag.type !== "text") continue;
if (remaining <= 0) {
frag.text = "";
continue;
}
const pruned = pruneTextEnd(frag.text, remaining, pad$1);
if (pruned !== frag.text) {
frag.text = pruned;
remaining = 0;
continue;
}
remaining -= width(frag.text);
}
return fragments.map((frag) => frag.text).join("");
}
/**
* Prune the start of a string to fit within a specified width, adding an ellipsis if necessary.
* @param str - the text to prune - ANSI is supported
* @param maxWidth - the maximum width of the text
* @param pad - the string to use for padding, default is '…'
* @returns the pruned text
*/
function pruneAnsiTextStart(str, maxWidth$2, pad$1 = "…") {
if (!maxWidth$2 || maxWidth$2 <= 0) return str;
if (str.length <= maxWidth$2) return str;
if (ansiWidth(str) <= maxWidth$2) return str;
const padWidth$1 = ansiWidth(pad$1);
const fragments = parseAnsiStr(str);
const maxWidthWithPad = maxWidth$2 - padWidth$1;
let remaining = maxWidthWithPad;
for (const frag of fragments.reverse()) {
if (frag.type !== "text") continue;
if (remaining <= 0) {
frag.text = "";
continue;
}
const pruned = pruneTextStart(frag.text, remaining, pad$1);
if (pruned !== frag.text) {
frag.text = pruned;
remaining = 0;
continue;
}
remaining -= width(frag.text);
}
return fragments.reverse().map((frag) => frag.text).join("");
}
//#endregion
//#region src/util/table.ts
function tableToLines(table, deliminator) {
const del = deliminator || table.deliminator || " | ";
const columnWidths = [];
const maxColumnWidthsMap = table.maxColumnWidths || {};
const { header, rows } = table;
const simpleHeader = header.map((col) => Array.isArray(col) ? col[1] : col);
const columnFieldNames = header.map((col) => Array.isArray(col) ? col[0] : col);
const maxColumnWidths = columnFieldNames.map((field, idx) => maxColumnWidthsMap[field] ?? maxColumnWidthsMap[idx]);
function getCell(row, col) {
return getCellFromRow(rows[row], col);
}
function getCellFromRow(row, col) {
if (!row) return void 0;
if (Array.isArray(row)) return row[col];
const fieldName = columnFieldNames[col];
return row[fieldName];
}
function rowToCells(row) {
if (Array.isArray(row)) return row;
return columnFieldNames.map((fieldName) => row[fieldName]);
}
function getText(col, maxWidth$2) {
return !col ? "" : typeof col === "string" ? pruneTextEnd(col, maxWidth$2) : col(maxWidth$2);
}
function getRCText(row, col, maxWidth$2) {
return getText(getCell(row, col), maxWidth$2);
}
function recordHeaderWidths(header$1) {
header$1.forEach((col, idx) => {
columnWidths[idx] = Math.max(ansiWidth(col), columnWidths[idx] || 0);
});
}
function recordColWidths() {
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) for (let colIndex = 0; colIndex < columnFieldNames.length; colIndex++) columnWidths[colIndex] = Math.max(ansiWidth(getRCText(rowIndex, colIndex, void 0)), columnWidths[colIndex] || 0);
}
function justifyRow(c, i) {
return pad(c, columnWidths[i]);
}
function toHeaderLine(header$1) {
return decorateRowWith(header$1.map((c, i) => getText(c, columnWidths[i])), justifyRow, headerDecorator).join(del);
}
function toLine(row) {
return decorateRowWith(rowToCells(row).map((c, i) => getText(c, columnWidths[i])), justifyRow).join(del);
}
function* process$1() {
yield toHeaderLine(simpleHeader);
yield* rows.map(toLine);
}
function sumColumnWidths() {
return columnWidths.reduce((sum, width$1) => sum + width$1, 0);
}
function adjustColWidths() {
for (let i = 0; i < columnWidths.length; i++) {
const mw = maxColumnWidths[i];
if (!mw) continue;
columnWidths[i] = Math.min(columnWidths[i], mw);
}
if (!table.terminalWidth) return;
const dWidth = (columnWidths.length - 1) * ansiWidth(del);
const lineWidth = table.terminalWidth - dWidth;
if (lineWidth <= columnWidths.length * 2) {
const fixedWidth = Math.max(Math.min(...columnWidths), 5);
for (let i = 0; i < columnWidths.length; i++) columnWidths[i] = fixedWidth;
return;
}
if (columnWidths.length === 1) {
columnWidths[0] = lineWidth;
return;
}
function trimWidestColumn(neededToTrim) {
let first = 0;
let second = 0;
for (let i = 0; i < columnWidths.length; i++) if (columnWidths[i] > columnWidths[first]) {
second = first;
first = i;
} else if (columnWidths[i] > columnWidths[second]) second = i;
const diff$1 = Math.max(columnWidths[first] - columnWidths[second], 1);
columnWidths[first] -= Math.min(diff$1, neededToTrim);
}
for (let sum = sumColumnWidths(); sum > lineWidth; sum = sumColumnWidths()) trimWidestColumn(sum - lineWidth);
}
recordHeaderWidths(simpleHeader);
recordColWidths();
adjustColWidths();
return [...process$1()];
}
function headerDecorator(t) {
return chalk.bold(chalk.underline(t));
}
function decorateRowWith(row, ...decorators) {
return decorators.reduce((row$1, decorator) => row$1.map(decorator), row);
}
//#endregion
//#region src/emitters/helpers.ts
function trimMidPath(s, w, sep$1) {
if (s.length <= w) return s;
const parts = s.split(sep$1);
if (parts[parts.length - 1].length > w) return trimMid(s, w);
function join(left$1, right$1) {
return [
...parts.slice(0, left$1),
"…",
...parts.slice(right$1)
].join(sep$1);
}
let left = 0, right = parts.length, last = "";
for (let i = 0; i < parts.length; ++i) {
const incLeft = i & 1 ? 1 : 0;
const incRight = incLeft ? 0 : -1;
const next = join(left + incLeft, right + incRight);
if (next.length > w) break;
left += incLeft;
right += incRight;
last = next;
}
for (let i = left + 1; i < right; ++i) {
const next = join(i, right);
if (next.length > w) break;
last = next;
}
for (let i = right - 1; i > left; --i) {
const next = join(left, i);
if (next.length > w) break;
last = next;
}
return last || trimMid(s, w);
}
function trimMid(s, w) {
s = s.trim();
if (s.length <= w) return s;
const l = Math.floor((w - 1) / 2);
const r = Math.ceil((w - 1) / 2);
return s.slice(0, l) + "…" + s.slice(-r);
}
function formatDictionaryLocation(dictSource, maxWidth$2, { cwd, dictionaryPathFormat: format$1, iPath: iPath$1 }) {
let relPath = cwd ? iPath$1.relative(cwd, dictSource) : dictSource;
const idxNodeModule = relPath.lastIndexOf("node_modules");
const isNodeModule = idxNodeModule >= 0;
if (format$1 === "hide") return "";
if (format$1 === "short") {
const prefix = isNodeModule ? "[node_modules]/" : relPath.startsWith(".." + iPath$1.sep + "..") ? "…/" : relPath.startsWith(".." + iPath$1.sep) ? "../" : "";
return prefix + iPath$1.basename(dictSource);
}
if (format$1 === "full") return dictSource;
relPath = isNodeModule ? relPath.slice(idxNodeModule) : relPath;
const usePath = relPath.length < dictSource.length ? relPath : dictSource;
return trimMidPath(usePath, maxWidth$2, iPath$1.sep);
}
//#endregion
//#region src/emitters/dictionaryListEmitter.ts
const maxWidth$1 = 120;
function emitListDictionariesResults(results, options) {
const report = calcListDictsResultsReport(results, options);
console.log(report.table);
if (report.errors) {
console.error("Errors:");
console.error(report.errors);
}
}
function calcListDictsResultsReport(results, options) {
if (options.color === true) chalk.level = 2;
else if (options.color === false) chalk.level = 0;
const col = new Intl.Collator();
results.sort((a, b) => col.compare(a.name, b.name));
const header = calcHeaders(options);
const rows = results.map((r) => dictTableRowToTableRow(emitDictResult(r, options)));
const t = tableToLines({
header,
rows,
terminalWidth: options.lineWidth || process.stdout.columns || maxWidth$1,
deliminator: " ",
maxColumnWidths: {
locales: 12,
fileTypes: 40
}
});
return {
table: t.map((line) => line.trimEnd()).join("\n"),
errors: ""
};
}
function calcHeaders(options) {
const showLocation = options.dictionaryPathFormat !== "hide" && (options.options.showLocation ?? true);
const showLocales = options.options.showLocales ?? true;
const showFileTypes = options.options.showFileTypes ?? true;
const headers = [["name", "Dictionary"]];
showLocales && headers.push(["locales", "Locales"]);
showFileTypes && headers.push(["fileTypes", "File Types"]);
showLocation && headers.push(["location", "Dictionary Location"]);
return headers;
}
function emitDictResult(r, options) {
const a = r.enabled ? "*" : " ";
const dictColor = r.enabled ? chalk.yellowBright : chalk.rgb(200, 128, 50);
const n = (width$1) => dictColor(pruneAnsiTextEnd(r.name, width$1 && width$1 - a.length) + a);
const c = colorize$1(chalk.white);
const locales = (width$1) => c(pruneAnsiTextEnd(r.locales?.join(",") || "", width$1));
const fileTypes = (width$1) => c(pruneAnsiTextEnd(r.fileTypes?.join(",") || "", width$1));
if (!r.path) return {
name: n,
location: c(r.inline?.join(", ") || ""),
locales,
fileTypes
};
return {
name: n,
location: (widthSrc) => c(r.path && pruneAnsiTextStart(formatDictionaryLocation(r.path, widthSrc ?? maxWidth$1, {
iPath,
...options
}), widthSrc ?? maxWidth$1) || ""),
locales,
fileTypes
};
}
function dictTableRowToTableRow(row) {
return Object.fromEntries(Object.entries(row));
}
function colorize$1(fn) {
return (s) => s ? fn(s) : "";
}
//#endregion
//#region src/emitters/DictionaryPathFormat.ts
const formats = {
full: true,
hide: true,
long: true,
short: true
};
function isDictionaryPathFormat(value) {
if (!value || typeof value !== "string") return false;
return value in formats;
}
//#endregion
//#region src/util/canUseColor.ts
function canUseColor(colorOption) {
if (colorOption !== void 0) return colorOption;
if (!("NO_COLOR" in process.env)) return void 0;
if (!process.env["NO_COLOR"] || process.env["NO_COLOR"] === "false") return void 0;
return false;
}
//#endregion
//#region src/commandDictionaries.ts
function commandDictionaries(prog) {
return prog.command("dictionaries").description(`List dictionaries`).option("-c, --config <cspell.json>", "Configuration file to use. By default cspell looks for cspell.json in the current directory.").addOption(crOpt("--path-format <format>", "Configure how to display the dictionary path.").choices([
"hide",
"short",
"long",
"full"
]).default("long", "Display most of the path.")).addOption(crOpt("--enabled", "Show only enabled dictionaries.").default(void 0)).addOption(crOpt("--no-enabled", "Do not show enabled dictionaries.")).option("--locale <locale>", "Set language locales. i.e. \"en,fr\" for English and French, or \"en-GB\" for British English.").option("--file-type <fileType>", "File type to use. i.e. \"html\", \"golang\", or \"javascript\".").option("--no-show-location", "Do not show the location of the dictionary.").option("--show-file-types", "Show the file types supported by the dictionary.", false).addOption(crOpt("--no-show-file-types", "Do not show the file types supported by the dictionary.").hideHelp()).option("--show-locales", "Show the language locales supported by the dictionary.", false).addOption(crOpt("--no-show-locales", "Do not show the locales supported by the dictionary.").hideHelp()).addOption(crOpt("--color", "Force color.").default(void 0)).addOption(crOpt("--no-color", "Turn off color.").default(void 0)).addOption(crOpt("--default-configuration", "Load the default configuration and dictionaries.").hideHelp()).addOption(crOpt("--no-default-configuration", "Do not load the default configuration and dictionaries.")).action(async (options) => {
const dictionaryPathFormat = isDictionaryPathFormat(options.pathFormat) ? options.pathFormat : "long";
const useColor = canUseColor(options.color);
const listResult = await listDictionaries(options);
emitListDictionariesResults(listResult, {
cwd: process.cwd(),
dictionaryPathFormat,
color: useColor,
options
});
});
}
//#endregion
//#region src/commandInit.ts
function commandInit(prog) {
const command = prog.command("init").description("Initialize a CSpell configuration file.").addOption(crOpt("-c, --config <path>", "Path to the CSpell configuration file. Conflicts with --output and --format.").conflicts(["output", "format"])).option("-o, --output <path>", "Define where to write file.").addOption(crOpt("--format <format>", "Define the format of the file.").choices([
"yaml",
"yml",
"json",
"jsonc"
]).default("yaml")).option("--import <path|package>", "Import a configuration file or dictionary package.", collect).option("--locale <locale>", "Define the locale to use when spell checking (e.g., en, en-US, de).").addOption(crOpt("--dictionary <dictionary>", "Enable a dictionary. Can be used multiple times.", collect).default(void 0)).addOption(crOpt("--comments", "Add comments to the config file.").default(void 0).hideHelp()).option("--no-comments", "Do not add comments to the config file.").addOption(crOpt("--remove-comments", "Remove all comments from the config file.").implies({ comments: false })).option("--no-schema", "Do not add the schema reference to the config file.").option("--stdout", "Write the configuration to stdout instead of a file.").action((options) => {
return createInit(options);
});
return command;
}
//#endregion
//#region src/link.ts
const listGlobalImports = Link.listGlobalImports;
const addPathsToGlobalImports = Link.addPathsToGlobalImports;
const removePathsFromGlobalImports = Link.removePathsFromGlobalImports;
function listGlobalImportsResultToTable(results) {
const header = [
"id",
"package",
"name",
"filename",
"dictionaries",
"errors"
];
const decorate = (isError) => isError ? (s) => chalk.red(s) : (s) => s;
function toColumns(r) {
return [
r.id,
r.package?.name,
r.name,
r.filename,
r.dictionaryDefinitions?.map((def) => def.name).join(", "),
r.error ? "Failed to read file." : ""
].map((c) => c || "").map(decorate(!!r.error));
}
return {
header,
rows: results.map(toColumns)
};
}
function addPathsToGlobalImportsResultToTable(results) {
const header = ["filename", "errors"];
const decorate = (isError) => isError ? (s) => chalk.red(s) : (s) => s;
function toColumns(r) {
return [r.resolvedToFilename || r.filename, r.error ? "Failed to read file." : ""].map((c) => c || "").map(decorate(!!r.error));
}
return {
header,
rows: results.resolvedSettings.map(toColumns)
};
}
//#endregion
//#region src/commandLink.ts
function commandLink(prog) {
const linkCommand = prog.command("link").description("Link dictionaries and other settings to the cspell global config.");
linkCommand.command("list", { isDefault: true }).alias("ls").description("List currently linked configurations.").action(async () => {
const imports = await listGlobalImports();
const table = listGlobalImportsResultToTable(imports.list);
tableToLines(table).forEach((line) => console.log(line));
return;
});
linkCommand.command("add <dictionaries...>").alias("a").description("Add dictionaries any other settings to the cspell global config.").action(async (dictionaries) => {
const r = await addPathsToGlobalImports(dictionaries);
const table = addPathsToGlobalImportsResultToTable(r);
console.log("Adding:");
tableToLines(table).forEach((line) => console.log(line));
if (r.error) throw new CheckFailed(r.error, 1);
return;
});
linkCommand.command("remove <paths...>").alias("r").description("Remove matching paths / packages from the global config.").action(async (dictionaries) => {
const r = await removePathsFromGlobalImports(dictionaries);
console.log("Removing:");
if (r.error) throw new CheckFailed(r.error, 1);
r.removed.map((f) => console.log(f));
return;
});
return linkCommand;
}
//#endregion
//#region src/util/unindent.ts
/**
* Inject values into a template string.
* @param {TemplateStringsArray} template
* @param {...any} values
* @returns
*/
function _inject(template, ...values) {
const strings = template;
const adjValues = [];
for (let i = 0; i < values.length; ++i) {
const prevLines = strings[i].split("\n");
const currLine = prevLines[prevLines.length - 1];
const padLen = padLength(currLine);
const padding = " ".repeat(padLen);
const value = `${values[i]}`;
let pad$1 = "";
const valueLines = [];
for (const line of value.split("\n")) {
valueLines.push(pad$1 + line);
pad$1 = padding;
}
adjValues.push(valueLines.join("\n"));
}
return _unindent(String.raw({ raw: strings }, ...adjValues));
}
/**
* Calculate the padding at the start of the string.
* @param {string} s
* @returns {number}
*/
function padLength(s) {
return s.length - s.trimStart().length;
}
function unindent(template, ...values) {
if (typeof template === "string") return _unindent(template);
return _inject(template, ...values);
}
/**
* Remove the left padding from a multi-line string.
* @param {string} str
* @returns {string}
*/
function _unindent(str) {
const lines = str.split("\n");
let curPad = str.length;
for (const line of lines) {
if (!line.trim()) continue;
curPad = Math.min(curPad, padLength(line));
}
return lines.map((line) => line.slice(curPad)).join("\n");
}
//#endregion
//#region src/commandLint.ts
const usage = `\
[options] [globs...] [file://<path> ...] [stdin[://<path>]]
Patterns:
- [globs...] Glob Patterns
- [stdin] Read from "stdin" assume text file.
- [stdin://<path>] Read from "stdin", use <path> for file type and config.
- [file://<path>] Check the file at <path>
Examples:
cspell . Recursively check all files.
cspell lint . The same as "cspell ."
cspell "*.js" Check all .js files in the current directory
cspell "**/*.js" Check all .js files recursively
cspell "src/**/*.js" Only check .js under src
cspell "**/*.txt" "**/*.js" Check both .js and .txt files.
cspell "**/*.{txt,js,md}" Check .txt, .js, and .md files.
cat LICENSE | cspell stdin Check stdin
cspell stdin://docs/doc.md Check stdin as if it was "./docs/doc.md"\
`;
const advanced = `
More Examples:
cspell "**/*.js" --reporter @cspell/cspell-json-reporter
This will spell check all ".js" files recursively and use
"@cspell/cspell-json-reporter".
cspell . --reporter default
This will force the default reporter to be used overriding
any reporters defined in the configuration.
cspell . --reporter ./<path>/reporter.cjs
Use a custom reporter. See API for details.
cspell "*.md" --exclude CHANGELOG.md --files README.md CHANGELOG.md
Spell check only check "README.md" but NOT "CHANGELOG.md".
cspell "/*.md" --no-must-find-files --files $FILES
Only spell check the "/*.md" files in $FILES,
where $FILES is a shell variable that contains the list of files.
cspell --help --verbose
Show all options including hidden options.
References:
https://cspell.org
https://github.com/streetsidesoftware/cspell
`;
function commandLint(prog) {
const spellCheckCommand = prog.command("lint", { isDefault: true });
spellCheckCommand.description("Check spelling").option("-c, --config <cspell.json>", "Configuration file to use. By default cspell looks for cspell.json in the current directory.").addOption(crOpt("--config-search", "Allow searching for configuration files.", void 0).hideHelp()).option("--no-config-search", "Disable automatic searching for additional configuration files in parent directories. Only the specified config file (if any) will be used.").option("--stop-config-search-at <dir>", "Specify a directory at which to stop searching for configuration files when walking up from the files being checked. Useful for limiting config inheritance.", collect).option("-v, --verbose", "Display more information about the files being checked and the configuration.").option("--locale <locale>", "Set language locales. i.e. \"en,fr\" for English and French, or \"en-GB\" for British English.").option("--language-id <file-type>", "Force programming language for unknown extensions. i.e. \"php\" or \"scala\"").addOption(crOpt("--languageId <file-type>", "Alias of \"--language-id\". Force programming language for unknown extensions. i.e. \"php\" or \"scala\"").hideHelp()).option("--words-only", "Only output the words not found in the dictionaries.").addOption(crOpt("--wordsOnly", "Only output the words not found in the dictionaries.").hideHelp()).option("-u, --unique", "Only output the first instance of a word not found in the dictionaries.").option("-e, --exclude <glob>", "Exclude files matching the glob pattern. This option can be used multiple times to add multiple globs. ", collect).option("--file-list <path or stdin>", "Specify a list of files to be spell checked. The list is filtered against the glob file patterns. Note: the format is 1 file path per line.", collect).option("--file [file...]", "Specify files to spell check. They are filtered by the [globs...].", collect).addOption(crOpt("--files [file...]", "Alias of \"--file\". Files to spell check.", collect).hideHelp()).option("--no-issues", "Do not show the spelling errors.").option("--no-progress", "Turn off progress messages").option("--no-summary", "Turn off summary message in console.").option("-s, --silent", "Silent mode, suppress error messages.").option("--no-exit-code", "Do not return an exit code if issues are found.").addOption(crOpt("--quiet", "Only show spelling issues or errors.").implies({
summary: false,
progress: false
})).option("--fail-fast", "Exit after first file with an issue or error.").addOption(crOpt("--no-fail-fast", "Process all files even if there is an error.").hideHelp()).option("--continue-on-error", "Continue processing files even if there is a configuration error.").option("-r, --root <root folder>", "Root directory, defaults to current directory.").addOption(crOpt("--relative", "Issues are displayed relative to the root.").default(true).hideHelp()).option("--no-relative", "Issues are displayed with absolute path instead of relative to the root.").option("--show-context", "Show the surrounding text around an issue.").option("--show-suggestions", "Show spelling suggestions.").addOption(crOpt("--no-show-suggestions", "Do not show spelling suggestions or fixes.").default(void 0)).addOption(crOpt("--must-find-files", "Error if no files are found.").default(true).hideHelp()).option("--no-must-find-files", "Do not error if no files are found.").addOption(crOpt("--legacy", "Legacy output").hideHelp()).addOption(crOpt("--local <local>", "Deprecated -- Use: --locale").hideHelp()).option("--cache", "Use cache to only check changed files.").option("--no-cache", "Do not use cache.").option("--cache-reset", "Reset the cache file.").addOption(crOpt("--cache-strategy <strategy>", "Strategy to use for detecting changed files.").choices(["content", "metadata"]).default("content")).option("--cache-location <path>", `Path to the cache file or directory. (default: "${DEFAULT_CACHE_LOCATION}")`).option("--dot", "Include files and directories starting with `.` (period) when matching globs.").option("--gitignore", "Ignore files matching glob patterns found in .gitignore files.").option("--no-gitignore", "Do NOT use .gitignore files.").option("--gitignore-root <path>", "Prevent searching for .gitignore files past root.", collect).option("--validate-directives", "Validate in-document CSpell directives.").addOption(crOpt("--no-validate-directives", "Do not validate in-document CSpell directives.").hideHelp()).addOption(crOpt("--color", "Force color.").default(void 0)).addOption(crOpt("--no-color", "Turn off color.").default(void 0)).addOption(crOpt("--default-configuration", "Load the default configuration and dictionaries.").hideHelp()).addOption(crOpt("--no-default-configuration", "Do not load the default configuration and dictionaries.")).option("--dictionary <name>", "Enable a dictionary by name.", collect).option("--disable-dictionary <name>", "Disable a dictionary by name.", collect).option("--reporter <module|path>", "Specify one or more reporters to use.", collect).addOption(crOpt("--report <level>", "Set how unknown words are reported").choices(ReportChoicesAll)).addOption(crOpt("--skip-validation", "Collect and process documents, but do not spell check.").implies({ cache: false }).hideHelp()).addOption(crOpt("--issues-summary-report", "Output a summary of issues found.").hideHelp()).addOption(crOpt("--show-perf-summary", "Output a performance summary report.").hideHelp()).option("--issue-template [template]", "Use a custom issue template. See --help --issue-template for details.").addOption(crOpt("--debug", "Output information useful for debugging cspell.json files.").hideHelp()).usage(usage).addHelpText("after", augmentCommandHelp).arguments("[globs...]").action(async (fileGlobs, options) => {
const useExitCode = options.exitCode ?? true;
if (options.skipValidation) options.cache = false;
options.color ??= canUseColor(options.color);
parseApplicationFeatureFlags(options.flag);
const { mustFindFiles, fileList, files, file } = options;
const result = await lint(fileGlobs, options);
if (!fileGlobs.length && !result.files && !result.errors && !fileList && !files?.length && !file?.length) {
spellCheckCommand.outputHelp();
throw new CheckFailed("outputHelp", 1);
}
if (result.errors || mustFindFiles && !result.files) throw new CheckFailed("check failed", 1);
if (result.issues) {
const exitCode = useExitCode ? 1 : 0;
throw new CheckFailed("check failed", exitCode);
}
return;
});
return spellCheckCommand;
}
function helpIssueTemplate(opts) {
if (!("issueTemplate" in opts)) return "";
return unindent`
Issue Template:
Use "--issue-template" to set the template to use when reporting issues.
The template is a string that can contain the following placeholders:
- $filename - the file name
- $col - the column number
- $row - the row number
- $text - the word that is misspelled
- $message - the issues message: "unknown word", "word is misspelled", etc.
- $messageColored - the issues message with color based upon the message type.
- $uri - the URI of the file
- $suggestions - suggestions for the misspelled word (if requested)
- $quickFix - possible quick fixes for the misspelled word.
- $contextFull - the full context of the misspelled word.
- $contextLeft - the context to the left of the misspelled word.
- $contextRight - the context to the right of the misspelled word.
Color is supported using the following template pattern:
- \`{<style[.style]> <text>}\` - where \`<style>\` is a style name and \`<text>\` is the text to style.
Styles
- \`bold\`, \`italic\`, \`underline\`, \`strikethrough\`, \`dim\`, \`inverse\`
- \`black\`, \`red\`, \`green\`, \`yellow\`, \`blue\`, \`magenta\`, \`cyan\`, \`white\`
Example:
--issue-template '{green $filename}:{yellow $row}:{yellow $col} $message {red $text} $quickFix {dim $suggestions}'
`;
}
/**
* Add additional help text to the command.
* When the verbose flag is set, show the hidden options.
* @param context
* @returns
*/
function augmentCommandHelp(context) {
const output = [];
const command = context.command;
const opts = command.opts();
const showHidden = !!opts.verbose;
const hiddenHelp = [];
const help = command.createHelp();
help.helpWidth = process.stdout.columns || 80;
const hiddenOptions = command.options.filter((opt) => opt.hidden && showHidden);
const flagColWidth = Math.max(...command.options.map((opt) => opt.flags.length), 0);
for (const options of hiddenOptions) {
if (!hiddenHelp.length) hiddenHelp.push("\nHidden Options:");
hiddenHelp.push(help.formatItem(options.flags, flagColWidth, options.description, help));
}
output.push(...hiddenHelp, advanced);
return helpIssueTemplate(opts) + output.join("\n");
}
//#endregion
//#region src/emitters/suggestionsEmitter.ts
const regExpRTL = /([ \u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]+)/g;
function reverseRtlText(s) {
return s.replaceAll(regExpRTL, (s$1) => [...s$1].reverse().join(""));
}
function emitSuggestionResult(result, options) {
const { word, suggestions: suggestions$1 } = result;
const { verbose, output = console } = options;
const elapsed = verbose && verbose > 1 && result.elapsedTimeMs ? ` ${result.elapsedTimeMs.toFixed(2)} ms` : "";
const rWord = reverseRtlText(word);
const wordEx = rWord !== word ? ` (${chalk.yellow(rWord)})` : "";
output.log((word ? chalk.yellow(word) + wordEx : chalk.yellow("<empty>")) + ":" + elapsed);
if (!suggestions$1.length) {
console.log(chalk.yellow(" <no suggestions>"));
return;
}
function handleRtl(word$1) {
const r = reverseRtlText(word$1);
return r === word$1 ? word$1 : `${word$1} (${r})`;
}
if (verbose) {
const mappedSugs = suggestions$1.map((s) => ({
...s,
w: handleRtl(s.compoundWord || s.wordAdjustedToMatchCase || s.word)
}));
const sugWidths = mappedSugs.map((s) => width(s.w));
const maxWidth$2 = sugWidths.reduce((max, len) => Math.max(max, len), 0);
for (const sug of mappedSugs) {
const { cost, dictionaries, w } = sug;
const padding = " ".repeat(padWidth(w, maxWidth$2));
const forbid = sug.forbidden && sug.isPreferred ? chalk.red("*") : sug.forbidden ? chalk.red("X") : sug.isPreferred ? chalk.yellow("*") : " ";
const ignore = sug.noSuggest ? chalk.yellow("N") : " ";
const strCost = padLeft(cost.toString(10), 4);
const dicts = dictionaries.map((n) => chalk.gray(n)).join(", ");
output.log(` - ${formatWord(w, sug)}${padding} ${forbid}${ignore} - ${chalk.yellow(strCost)} ${dicts}`);
}
} else {
const mappedSugs = suggestions$1.map((s) => ({
...s,
word: handleRtl(s.wordAdjustedToMatchCase || s.word)
}));
for (const r of mappedSugs) output.log(` - ${formatWordSingle(r)}`);
}
}
function formatWord(word, r) {
return r.forbidden || r.noSuggest ? chalk.gray(chalk.strikethrough(word)) : word === r.wordAdjustedToMatchCase ? diff(word, r.word) : word;
}
function diff(wordA, wordB) {
const a = [...wordA];
const b = [...wordB];
const parts = [];
for (let idx = 0; idx < a.length; ++idx) {
const aa = a[idx];
const bb = b[idx];
parts.push(aa === bb ? aa : chalk.yellow(aa));
}
return parts.join("");
}
function formatWordSingle(s) {
let word = formatWord(s.word, s);
word = s.forbidden ? word + chalk.red(" X") : word;
word = s.noSuggest ? word + chalk.yellow(" Not suggested.") : word;
word = s.isPreferred ? chalk.yellow(word + " *") : word;
return word;
}
//#endregion
//#region src/commandSuggestion.ts
function collect$1(value, previous) {
value = value.replace(/^=/, "");
if (!previous) return [value];
return [...previous, value];
}
function count(_, previous) {
return (previous || 0) + 1;
}
function asNumber(value, prev) {
return Number.parseInt(value, 10) ?? prev;
}
function commandSuggestion(prog) {
const suggestionCommand = prog.command("suggestions");
suggestionCommand.aliases(["sug", "suggest"]).description("Spelling Suggestions for words.").option("-c, --config <cspell.json>", "Configuration file to use. By default cspell looks for cspell.json in the current directory.").option("--locale <locale>", "Set language locales. i.e. \"en,fr\" for English and French, or \"en-GB\" for British English.").option("--language-id <language>", "Use programming language. i.e. \"php\" or \"scala\".").addOption(new Option("--languageId <language>", "Use programming language. i.e. \"php\" or \"scala\".").hideHelp()).option("-s, --no-strict", "Ignore case and accents when searching for words.").option("--ignore-case", "Alias of --no-strict.").option("--num-changes <number>", "Number of changes allowed to a word", asNumber, 4).option("--num-suggestions <number>", "Number of suggestions", asNumber, 8).option("--no-include-ties", "Force the number of suggested to be limited, by not including suggestions that have the same edit cost.").option("--stdin", "Use stdin for input.").addOption(new Option("--repl", "REPL interface for looking up suggestions.")).option("-v, --verbose", "Show detailed output.", count, 0).option("-d, --dictionary <dictionary name>", "Use the dictionary specified. Only dictionaries specified will be used.", collect$1).option("--dictionaries <dictionary names...>", "Use the dictionaries specified. Only dictionaries specified will be used.").option("--no-color", "Turn off color.").option("--color", "Force color").arguments("[words...]").action(async (words, options) => {
parseApplicationFeatureFlags(options.flag);
options.useStdin = options.stdin;
options.dictionaries = mergeArrays(options.dictionaries, options.dictionary);
if (!words.length && !options.useStdin && !options.repl) {
suggestionCommand.outputHelp();
throw new CheckFailed("outputHelp", 1);
}
for await (const r of suggestions(words, options)) emitSuggestionResult(r, options);
});
return suggestionCommand;
}
function mergeArrays(a, b) {
if (a === void 0) return b;
if (b === void 0) return a;
return [...a, ...b];
}
//#endregion
//#region src/emitters/traceEmitter.ts
const maxWidth = 120;
const colWidthDictionaryName = 20;
function emitTraceResults(word, found, results, options) {
const report = calcTraceResultsReport(word, found, results, options);
console.log(report.table);
if (report.errors) {
console.error("Errors:");
console.error(report.errors);
}
}
function calcTraceResultsReport(word, found, results, options) {
if (options.color === true) chalk.level = 2;
else if (options.color === false) chalk.level = 0;
const col = new Intl.Collator();
results.sort((a, b) => col.compare(a.dictName, b.dictName));
options.showWordFound && console.log(`${options.prefix || ""}${word}: ${found ? "Found" : "Not Found"}`);
const header = emitHeader(options.dictionaryPathFormat !== "hide");
const rows = results.map((r) => emitTraceResult(r, options));
const t = tableToLines({
header,
rows,
terminalWidth: options.lineWidth || process.stdout.columns || maxWidth,
deliminator: " "
});
return {
table: t.map((line) => line.trimEnd()).join("\n"),
errors: emitErrors(results).join("\n")
};
}
function emitHeader(location) {
const headers = [
"Word",
"F",
"Dictionary"
];
location && headers.push("Dictionary Location");
return headers;
}
function emitTraceResult(r, options) {
const errors = !!r.errors?.length;
const word = r.foundWord || r.word;
const cWord = word.replaceAll("+", chalk.yellow("+"));
const sug = r.preferredSuggestions?.map((s) => chalk.yellowBright(s)).join(", ") || "";
const w = (r.forbidden ? chalk.red(cWord) : chalk.green(cWord)) + (sug ? `->(${sug})` : "");
const f = calcFoundChar(r);
const a = r.dictActive ? "*" : " ";
const dictName = r.dictName.slice(0, colWidthDictionaryName - 1) + a;
const dictColor = r.dictActive ? chalk.yellowBright : chalk.rgb(200, 128, 50);
const n = dictColor(dictName);
const c = colorize(errors ? chalk.red : chalk.white);
return [
w,
f,
n,
(widthSrc) => c(formatDictionaryLocation(r.dictSource, widthSrc ?? maxWidth, {
iPath,
...options
}))
];
}
function emitErrors(results) {
const errorResults = results.filter((r) => r.errors?.length);
return errorResults.map((r) => {
const errors = r.errors?.map((e) => e.message)?.join("\n ") || "";
return chalk.bold(r.dictName) + "\n " + chalk.red(errors);
});
}
function calcFoundChar(r) {
const errors = r.errors?.map((e) => e.message)?.join("\n ") || "";
let color = chalk.dim;
color = r.found ? chalk.whiteBright : color;
color = r.forbidden ? chalk.red : color;
color = r.noSuggest ? chalk.yellowBright : color;
color = errors ? chalk.red : color;
let char = "-";
char = r.found ? "*" : char;
char = r.forbidden ? "!" : char;
char = r.noSuggest ? "I" : char;
char = errors ? "X" : char;
return color(char);
}
function colorize(fn) {
return (s) => s ? fn(s) : "";
}
//#endregion
//#region src/commandTrace.ts
function commandTrace(prog) {
return prog.command("trace").description(`Trace words -- Search for words in the configuration and dictionaries.`).option("-c, --config <cspell.json>", "Configuration file to use. By default cspell looks for cspell.json in the current directory.").option("--locale <locale>", "Set language locales. i.e. \"en,fr\" for English and French, or \"en-GB\" for British English.").option("--language-id <language>", "Use programming language. i.e. \"php\" or \"scala\".").addOption(new Option("--languageId <language>", "Use programming language. i.e. \"php\" or \"scala\".").hideHelp()).option("--allow-compound-words", "Turn on allowCompoundWords").addOption(new Option("--allowCompoundWords", "Turn on allowCompoundWords.").hideHelp()).option("--no-allow-compound-words", "Turn off allowCompoundWords").option("--ignore-case", "Ignore case and accents when searching for words.").option("--no-ignore-case", "Do not ignore case and accents when searching for words.").option("--dictionary <name>", "Enable a dictionary by name. Can be used multiple times.", collect).addOption(new Option("--dictionary-path <format>", "Configure how to display the dictionary path.").choices([
"hide",
"short",
"long",
"full"
]).default("long", "Display most of the path.")).option("--stdin", "Read words from stdin.").option("--all", "Show all dictionaries.").addOption(new Option("--only-found", "Show only dictionaries that have the words.").conflicts("all")).addOption(new Option("--color", "Force color.").default(void 0)).addOption(new Option("--no-color", "Turn off color.").default(void 0)).addOption(new Option("--default-configuration", "Load the default configuration and dictionaries.").hideHelp()).addOption(new Option("--no-default-configuration", "Do not load the default configuration and dictionaries.")).arguments("[words...]").action(async (words, options) => {
parseApplicationFeatureFlags(options.flag);
let numFound = 0;
const dictionaryPathFormat = isDictionaryPathFormat(options.dictionaryPath) ? options.dictionaryPath : "long";
let prefix = "";
const useColor = canUseColor(options.color);
for await (const results of trace(words, options)) {
const byWord = groupBy(results, (r) => r.word);
for (const split of results.splits) {
const splitResults = byWord.get(split.word) || [];
const filtered = filterTraceResults(splitResults, options);
emitTraceResults(split.word, split.found, filtered, {
cwd: process.cwd(),
dictionaryPathFormat,
prefix,
showWordFound: results.splits.length > 1,
color: useColor
});
prefix = "\n";
numFound += results.reduce((n, r) => n + (r.found ? 1 : 0), 0);
const numErrors = results.map((r) => r.errors?.length || 0).reduce((n, r) => n + r, 0);
if (numErrors) {
console.error("Dictionary Errors.");
throw new CheckFailed("dictionary errors", 1);
}
}
}
if (!numFound) {
console.error("No matches found");
throw new CheckFailed("no matches", 1);
}
});
}
function filterTraceResults(results, options) {
if (options.all) return results;
return results.filter((r) => filterTraceResult(r, options.onlyFound));
}
function filterTraceResult(result, onlyFound) {
return result.found || result.forbidden || result.noSuggest || !!result.preferredSuggestions || !onlyFound && result.dictActive;
}
function groupBy(items, key) {
const map = /* @__PURE__ */ new Map();
for (const item of items) {
const k = key(item);
const a = map.get(k) || [];
a.push(item);
map.set(k, a);
}
return map;
}
//#endregion
//#region src/app.mts
async function run(command, argv) {
const prog = command || program;
const args = argv || process.argv;
prog.exitOverride();
prog.version(npmPackage.version).description("Spelling Checker for Code").name("cspell");
if (!satisfies(process.versions.node, npmPackage.engines.node)) throw new ApplicationError(`Unsupported NodeJS version (${process.versions.node}); ${npmPackage.engines.node} is required`);
const optionFlags = new Option("-f,--flag <flag:value>", "Declare an execution flag value").hideHelp().argParser((value, prev) => prev?.concat(value) || [value]);
commandLint(prog).addOption(optionFlags);
commandTrace(prog).addOption(optionFlags);
commandCheck(prog).addOption(optionFlags);
commandSuggestion(prog).addOption(optionFlags);
commandInit(prog).addOption(optionFlags);
commandLink(prog);
commandDictionaries(prog);
prog.exitOverride();
await prog.parseAsync(args);
}
//#endregion
export { ApplicationError, CheckFailed, run };
//# sourceMappingURL=app.js.map