UNPKG

cspell

Version:

A Spelling Checker for Code!

1,033 lines (1,000 loc) 47.8 kB
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