cspell 8.11.0 → 8.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +2 -0
  2. package/bin.mjs +4 -1
  3. package/dist/esm/app.d.mts +2 -2
  4. package/dist/esm/app.mjs +8 -8
  5. package/dist/esm/application.d.mts +2 -2
  6. package/dist/esm/application.mjs +13 -11
  7. package/dist/esm/cli-reporter.d.ts +26 -4
  8. package/dist/esm/cli-reporter.js +139 -68
  9. package/dist/esm/commandCheck.js +3 -2
  10. package/dist/esm/commandLink.js +1 -0
  11. package/dist/esm/commandLint.js +38 -3
  12. package/dist/esm/commandSuggestion.js +1 -1
  13. package/dist/esm/commandTrace.js +2 -1
  14. package/dist/esm/console.d.ts +25 -0
  15. package/dist/esm/console.js +53 -0
  16. package/dist/esm/dirname.d.ts +2 -0
  17. package/dist/esm/dirname.js +13 -0
  18. package/dist/esm/emitters/suggestionsEmitter.js +1 -0
  19. package/dist/esm/emitters/traceEmitter.d.ts +1 -1
  20. package/dist/esm/emitters/traceEmitter.js +1 -0
  21. package/dist/esm/featureFlags/featureFlags.js +1 -0
  22. package/dist/esm/index.d.mts +3 -2
  23. package/dist/esm/index.mjs +1 -1
  24. package/dist/esm/lint/lint.js +6 -4
  25. package/dist/esm/models.d.ts +15 -0
  26. package/dist/esm/models.js +2 -0
  27. package/dist/esm/options.d.ts +29 -0
  28. package/dist/esm/pkgInfo.d.ts +14 -0
  29. package/dist/esm/pkgInfo.js +7 -0
  30. package/dist/{lib/file-entry-cache.d.cts → esm/util/cache/file-entry-cache.d.mts} +2 -2
  31. package/dist/esm/util/cache/file-entry-cache.mjs +5 -0
  32. package/dist/esm/util/cache/fileEntryCache.d.ts +2 -2
  33. package/dist/esm/util/cache/fileEntryCache.js +3 -3
  34. package/dist/esm/util/fileHelper.d.ts +2 -3
  35. package/dist/esm/util/fileHelper.js +11 -14
  36. package/dist/esm/util/reporters.js +1 -1
  37. package/dist/esm/util/unindent.d.ts +14 -0
  38. package/dist/esm/util/unindent.js +55 -0
  39. package/package.json +17 -17
  40. package/dist/esm/app.d.ts +0 -5
  41. package/dist/esm/app.js +0 -45
  42. package/dist/esm/application.d.ts +0 -15
  43. package/dist/esm/application.js +0 -88
  44. package/dist/esm/cli-reporter.d.mts +0 -16
  45. package/dist/esm/cli-reporter.mjs +0 -304
  46. package/dist/esm/commandCheck.d.mts +0 -3
  47. package/dist/esm/commandCheck.mjs +0 -51
  48. package/dist/esm/commandLink.d.mts +0 -3
  49. package/dist/esm/commandLink.mjs +0 -47
  50. package/dist/esm/commandLint.d.mts +0 -3
  51. package/dist/esm/commandLint.mjs +0 -202
  52. package/dist/esm/commandSuggestion.d.mts +0 -3
  53. package/dist/esm/commandSuggestion.mjs +0 -61
  54. package/dist/esm/commandTrace.d.mts +0 -3
  55. package/dist/esm/commandTrace.mjs +0 -85
  56. package/dist/esm/emitters/DictionaryPathFormat.d.mts +0 -3
  57. package/dist/esm/emitters/DictionaryPathFormat.mjs +0 -12
  58. package/dist/esm/emitters/suggestionsEmitter.d.mts +0 -13
  59. package/dist/esm/emitters/suggestionsEmitter.mjs +0 -78
  60. package/dist/esm/emitters/traceEmitter.d.mts +0 -27
  61. package/dist/esm/emitters/traceEmitter.mjs +0 -148
  62. package/dist/esm/featureFlags/featureFlags.d.mts +0 -4
  63. package/dist/esm/featureFlags/featureFlags.mjs +0 -20
  64. package/dist/esm/featureFlags/index.d.mts +0 -2
  65. package/dist/esm/featureFlags/index.mjs +0 -2
  66. package/dist/esm/index.d.ts +0 -5
  67. package/dist/esm/index.js +0 -4
  68. package/dist/esm/link.d.mts +0 -8
  69. package/dist/esm/link.mjs +0 -39
  70. package/dist/esm/lint/LintRequest.d.mts +0 -25
  71. package/dist/esm/lint/LintRequest.mjs +0 -47
  72. package/dist/esm/lint/index.d.mts +0 -3
  73. package/dist/esm/lint/index.mjs +0 -3
  74. package/dist/esm/lint/lint.d.mts +0 -4
  75. package/dist/esm/lint/lint.mjs +0 -508
  76. package/dist/esm/options.d.mts +0 -211
  77. package/dist/esm/options.mjs +0 -8
  78. package/dist/esm/repl/index.d.mts +0 -18
  79. package/dist/esm/repl/index.mjs +0 -52
  80. package/dist/esm/util/InMemoryReporter.d.mts +0 -28
  81. package/dist/esm/util/InMemoryReporter.mjs +0 -42
  82. package/dist/esm/util/async.d.mts +0 -3
  83. package/dist/esm/util/async.mjs +0 -4
  84. package/dist/esm/util/cache/CSpellLintResultCache.d.mts +0 -20
  85. package/dist/esm/util/cache/CSpellLintResultCache.mjs +0 -2
  86. package/dist/esm/util/cache/CacheOptions.d.mts +0 -34
  87. package/dist/esm/util/cache/CacheOptions.mjs +0 -2
  88. package/dist/esm/util/cache/DiskCache.d.mts +0 -63
  89. package/dist/esm/util/cache/DiskCache.mjs +0 -214
  90. package/dist/esm/util/cache/DummyCache.d.mts +0 -11
  91. package/dist/esm/util/cache/DummyCache.mjs +0 -18
  92. package/dist/esm/util/cache/ObjectCollection.d.mts +0 -17
  93. package/dist/esm/util/cache/ObjectCollection.mjs +0 -127
  94. package/dist/esm/util/cache/createCache.d.mts +0 -31
  95. package/dist/esm/util/cache/createCache.mjs +0 -69
  96. package/dist/esm/util/cache/fileEntryCache.d.mts +0 -9
  97. package/dist/esm/util/cache/fileEntryCache.mjs +0 -79
  98. package/dist/esm/util/cache/index.d.mts +0 -4
  99. package/dist/esm/util/cache/index.mjs +0 -2
  100. package/dist/esm/util/constants.d.mts +0 -5
  101. package/dist/esm/util/constants.mjs +0 -5
  102. package/dist/esm/util/errors.d.mts +0 -23
  103. package/dist/esm/util/errors.mjs +0 -55
  104. package/dist/esm/util/fileHelper.d.mts +0 -65
  105. package/dist/esm/util/fileHelper.mjs +0 -161
  106. package/dist/esm/util/glob.d.mts +0 -45
  107. package/dist/esm/util/glob.mjs +0 -137
  108. package/dist/esm/util/pad.d.mts +0 -6
  109. package/dist/esm/util/pad.mjs +0 -28
  110. package/dist/esm/util/prefetch.d.mts +0 -2
  111. package/dist/esm/util/prefetch.mjs +0 -15
  112. package/dist/esm/util/reporters.d.mts +0 -14
  113. package/dist/esm/util/reporters.mjs +0 -62
  114. package/dist/esm/util/stdin.d.mts +0 -2
  115. package/dist/esm/util/stdin.mjs +0 -5
  116. package/dist/esm/util/table.d.mts +0 -14
  117. package/dist/esm/util/table.mjs +0 -55
  118. package/dist/esm/util/timer.d.mts +0 -4
  119. package/dist/esm/util/timer.mjs +0 -9
  120. package/dist/esm/util/types.d.mts +0 -7
  121. package/dist/esm/util/types.mjs +0 -5
  122. package/dist/esm/util/util.d.mts +0 -9
  123. package/dist/esm/util/util.mjs +0 -25
  124. package/dist/lib/file-entry-cache.cjs +0 -32
  125. package/dist/lib/pkgInfo.cjs +0 -6
  126. package/dist/lib/pkgInfo.d.cts +0 -3
package/README.md CHANGED
@@ -188,6 +188,8 @@ Options:
188
188
  --debug Output information useful for debugging
189
189
  cspell.json files.
190
190
  --reporter <module|path> Specify one or more reporters to use.
191
+ --issue-template [template] Use a custom issue template. See --help
192
+ --issue-template for details.
191
193
  -h, --help display help for command
192
194
 
193
195
  More Examples:
package/bin.mjs CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { format } from 'node:util';
3
+
2
4
  import { CommanderError, program } from 'commander';
3
5
 
4
6
  import * as app from './dist/esm/app.mjs';
5
7
 
6
8
  app.run(program, process.argv).catch((e) => {
7
9
  if (!(e instanceof CommanderError) && !(e instanceof app.CheckFailed)) {
8
- console.log(e);
10
+ const msg = e instanceof app.ApplicationError ? e.message : format(e);
11
+ process.stdout.write(msg + '\n');
9
12
  // It is possible an explicit exit code was set, use it if it was.
10
13
  process.exitCode = process.exitCode || 1;
11
14
  }
@@ -1,5 +1,5 @@
1
1
  import type { Command } from 'commander';
2
- export { LinterCliOptions as Options } from './options.mjs';
3
- export { CheckFailed } from './util/errors.mjs';
2
+ export { LinterCliOptions as Options } from './options.js';
3
+ export { ApplicationError, CheckFailed } from './util/errors.js';
4
4
  export declare function run(command?: Command, argv?: string[]): Promise<void>;
5
5
  //# sourceMappingURL=app.d.mts.map
package/dist/esm/app.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { Option as CommanderOption, program } from 'commander';
2
2
  import { satisfies as semverSatisfies } from 'semver';
3
- import { npmPackage } from '../lib/pkgInfo.cjs';
4
- import { commandCheck } from './commandCheck.mjs';
5
- import { commandLink } from './commandLink.mjs';
6
- import { commandLint } from './commandLint.mjs';
7
- import { commandSuggestion } from './commandSuggestion.mjs';
8
- import { commandTrace } from './commandTrace.mjs';
9
- import { ApplicationError } from './util/errors.mjs';
10
- export { CheckFailed } from './util/errors.mjs';
3
+ import { commandCheck } from './commandCheck.js';
4
+ import { commandLink } from './commandLink.js';
5
+ import { commandLint } from './commandLint.js';
6
+ import { commandSuggestion } from './commandSuggestion.js';
7
+ import { commandTrace } from './commandTrace.js';
8
+ import { npmPackage } from './pkgInfo.js';
9
+ import { ApplicationError } from './util/errors.js';
10
+ export { ApplicationError, CheckFailed } from './util/errors.js';
11
11
  export async function run(command, argv) {
12
12
  const prog = command || program;
13
13
  const args = argv || process.argv;
@@ -1,7 +1,7 @@
1
1
  import type { CSpellReporter, RunResult } from '@cspell/cspell-types';
2
2
  import type { CheckTextInfo, FeatureFlags, TraceWordResult } from 'cspell-lib';
3
- import type { TimedSuggestionsForWordResult } from './emitters/suggestionsEmitter.mjs';
4
- import type { BaseOptions, LegacyOptions, LinterCliOptions, SuggestionOptions, TraceOptions } from './options.mjs';
3
+ import type { TimedSuggestionsForWordResult } from './emitters/suggestionsEmitter.js';
4
+ import type { BaseOptions, LegacyOptions, LinterCliOptions, SuggestionOptions, TraceOptions } from './options.js';
5
5
  export type { TraceResult } from 'cspell-lib';
6
6
  export { IncludeExcludeFlag } from 'cspell-lib';
7
7
  export type AppError = NodeJS.ErrnoException;
@@ -1,19 +1,21 @@
1
1
  import { opMap, opTap, pipeAsync, toAsyncIterable } from '@cspell/cspell-pipe';
2
2
  import { checkTextDocument, getDefaultSettings, getGlobalSettingsAsync, mergeSettings, SuggestionError, suggestionsForWords, traceWordsAsync, } from 'cspell-lib';
3
- import { getReporter } from './cli-reporter.mjs';
4
- import { getFeatureFlags, parseFeatureFlags } from './featureFlags/index.mjs';
5
- import { LintRequest, runLint } from './lint/index.mjs';
6
- import { fixLegacy } from './options.mjs';
7
- import { simpleRepl } from './repl/index.mjs';
8
- import { fileInfoToDocument, readConfig, readFileInfo } from './util/fileHelper.mjs';
9
- import { finalizeReporter } from './util/reporters.mjs';
10
- import { readStdin } from './util/stdin.mjs';
11
- import { getTimeMeasurer } from './util/timer.mjs';
12
- import * as util from './util/util.mjs';
3
+ import { getReporter } from './cli-reporter.js';
4
+ import { console } from './console.js';
5
+ import { getFeatureFlags, parseFeatureFlags } from './featureFlags/index.js';
6
+ import { LintRequest, runLint } from './lint/index.js';
7
+ import { fixLegacy } from './options.js';
8
+ import { simpleRepl } from './repl/index.js';
9
+ import { fileInfoToDocument, readConfig, readFileInfo } from './util/fileHelper.js';
10
+ import { finalizeReporter } from './util/reporters.js';
11
+ import { readStdin } from './util/stdin.js';
12
+ import { getTimeMeasurer } from './util/timer.js';
13
+ import * as util from './util/util.js';
13
14
  export { IncludeExcludeFlag } from 'cspell-lib';
14
15
  export function lint(fileGlobs, options, reporter) {
15
16
  options = fixLegacy(options);
16
- const cfg = new LintRequest(fileGlobs, options, finalizeReporter(reporter) ?? getReporter({ ...options, fileGlobs }, options));
17
+ const reporterOptions = { ...options, console };
18
+ const cfg = new LintRequest(fileGlobs, options, finalizeReporter(reporter) ?? getReporter({ ...options, fileGlobs }, reporterOptions));
17
19
  return runLint(cfg);
18
20
  }
19
21
  export async function* trace(words, options) {
@@ -1,14 +1,36 @@
1
- import type { Issue, ReporterConfiguration } from '@cspell/cspell-types';
1
+ import type { Issue } from '@cspell/cspell-types';
2
+ import type { ChalkInstance } from 'chalk';
3
+ import { CSpellReporterConfiguration } from './models.js';
2
4
  import type { LinterCliOptions } from './options.js';
3
5
  import type { FinalizedReporter } from './util/reporters.js';
6
+ export interface TemplateSubstitutions extends Record<string, string> {
7
+ $col: string;
8
+ $contextFull: string;
9
+ $contextLeft: string;
10
+ $contextRight: string;
11
+ $filename: string;
12
+ $padContext: string;
13
+ $padRowCol: string;
14
+ $row: string;
15
+ $suggestions: string;
16
+ $text: string;
17
+ $uri: string;
18
+ $quickFix: string;
19
+ $message: string;
20
+ $messageColored: string;
21
+ }
4
22
  export interface ReporterIssue extends Issue {
5
23
  filename: string;
6
24
  }
7
- export interface ReporterOptions extends Pick<LinterCliOptions, 'debug' | 'issues' | 'legacy' | 'progress' | 'relative' | 'root' | 'showContext' | 'showPerfSummary' | 'showSuggestions' | 'silent' | 'summary' | 'verbose' | 'wordsOnly'> {
25
+ interface IOChalk {
26
+ readonly chalk: ChalkInstance;
27
+ }
28
+ export interface ReporterOptions extends Pick<LinterCliOptions, 'debug' | 'issues' | 'issuesSummaryReport' | 'legacy' | 'progress' | 'relative' | 'root' | 'showContext' | 'showPerfSummary' | 'showSuggestions' | 'silent' | 'summary' | 'verbose' | 'wordsOnly'> {
8
29
  fileGlobs: string[];
9
30
  }
10
- export declare function getReporter(options: ReporterOptions, config?: ReporterConfiguration): FinalizedReporter;
11
- declare function formatIssue(templateStr: string, issue: ReporterIssue, maxIssueTextWidth: number): string;
31
+ export declare function getReporter(options: ReporterOptions, config?: CSpellReporterConfiguration): FinalizedReporter;
32
+ declare function formatIssue(io: IOChalk, templateStr: string, issue: ReporterIssue, maxIssueTextWidth: number): string;
33
+ export declare function checkTemplate(template: string): true | Error;
12
34
  export declare const __testing__: {
13
35
  formatIssue: typeof formatIssue;
14
36
  };
@@ -1,20 +1,21 @@
1
- import * as path from 'node:path';
1
+ import assert from 'node:assert';
2
2
  import { format } from 'node:util';
3
- import chalk from 'chalk';
4
- import chalkTemplate from 'chalk-template';
3
+ import { toFileDirURL, toFilePathOrHref, toFileURL, urlRelative } from '@cspell/url';
4
+ import { Chalk } from 'chalk';
5
+ import { makeTemplate } from 'chalk-template';
5
6
  import { isSpellingDictionaryLoadError } from 'cspell-lib';
6
- import { URI } from 'vscode-uri';
7
+ import { console as customConsole } from './console.js';
8
+ import { ApplicationError } from './util/errors.js';
7
9
  import { uniqueFilterFnGenerator } from './util/util.js';
8
10
  const templateIssue = `{green $filename}:{yellow $row:$col} - $message ({red $text}) $quickFix`;
9
11
  const templateIssueNoFix = `{green $filename}:{yellow $row:$col} - $message ({red $text})`;
10
12
  const templateIssueWithSuggestions = `{green $filename}:{yellow $row:$col} - $message ({red $text}) Suggestions: {yellow [$suggestions]}`;
11
13
  const templateIssueWithContext = `{green $filename}:{yellow $row:$col} $padRowCol- $message ({red $text})$padContext -- {gray $contextLeft}{red {underline $text}}{gray $contextRight}`;
12
14
  const templateIssueWithContextWithSuggestions = `{green $filename}:{yellow $row:$col} $padRowCol- $message ({red $text})$padContext -- {gray $contextLeft}{red {underline $text}}{gray $contextRight}\n\t Suggestions: {yellow [$suggestions]}`;
13
- const templateIssueLegacy = `${chalk.green('$filename')}[$row, $col]: $message: ${chalk.red('$text')}`;
15
+ const templateIssueLegacy = `{green $filename}[$row, $col]: $message: {red $text}`;
14
16
  const templateIssueWordsOnly = '$text';
15
- function consoleError(...params) {
16
- console.error(chalk.white('%s'), format(...params));
17
- }
17
+ const console = undefined;
18
+ assert(!console);
18
19
  /**
19
20
  *
20
21
  * @param template - The template to use for the issue.
@@ -22,7 +23,7 @@ function consoleError(...params) {
22
23
  * @param reportedIssuesCollection - optional collection to store reported issues.
23
24
  * @returns issueEmitter function
24
25
  */
25
- function genIssueEmitter(template, uniqueIssues, reportedIssuesCollection) {
26
+ function genIssueEmitter(io, template, uniqueIssues, reportedIssuesCollection) {
26
27
  const uniqueFilter = uniqueIssues ? uniqueFilterFnGenerator((issue) => issue.text) : () => true;
27
28
  const defaultWidth = 10;
28
29
  let maxWidth = defaultWidth;
@@ -35,56 +36,69 @@ function genIssueEmitter(template, uniqueIssues, reportedIssuesCollection) {
35
36
  uri = issue.uri;
36
37
  }
37
38
  maxWidth = Math.max(maxWidth * 0.999, issue.text.length, 10);
38
- const issueText = formatIssue(template, issue, Math.ceil(maxWidth));
39
+ const issueText = formatIssue(io, template, issue, Math.ceil(maxWidth));
39
40
  reportedIssuesCollection?.push(issueText);
40
- console.log(issueText);
41
+ io.writeLine(issueText);
41
42
  };
42
43
  }
43
44
  function nullEmitter() {
44
45
  /* empty */
45
46
  }
46
- function relativeFilename(filename, cwd) {
47
- const rel = path.relative(cwd, filename);
48
- if (rel.startsWith('..'))
49
- return filename;
50
- return '.' + path.sep + rel;
51
- }
52
- function relativeUriFilename(uri, fsPathRoot) {
53
- const fsPath = URI.parse(uri).fsPath;
54
- const rel = path.relative(fsPathRoot, fsPath);
47
+ function relativeUriFilename(uri, rootURL) {
48
+ const url = toFileURL(uri);
49
+ const rel = urlRelative(rootURL, url);
55
50
  if (rel.startsWith('..'))
56
- return fsPath;
57
- return '.' + path.sep + rel;
51
+ return toFilePathOrHref(url);
52
+ return rel;
58
53
  }
59
- function reportProgress(p, cwd) {
54
+ function reportProgress(io, p, cwdURL, options) {
60
55
  if (p.type === 'ProgressFileComplete') {
61
- return reportProgressFileComplete(p);
56
+ return reportProgressFileComplete(io, p, cwdURL, options);
62
57
  }
63
58
  if (p.type === 'ProgressFileBegin') {
64
- return reportProgressFileBegin(p, cwd);
59
+ return reportProgressFileBegin(io, p, cwdURL);
65
60
  }
66
61
  }
67
- function reportProgressFileBegin(p, cwd) {
62
+ function determineFilename(io, p, cwd) {
68
63
  const fc = '' + p.fileCount;
69
64
  const fn = (' '.repeat(fc.length) + p.fileNum).slice(-fc.length);
70
65
  const idx = fn + '/' + fc;
71
- const filename = chalk.gray(relativeFilename(p.filename, cwd));
72
- process.stderr.write(`\r${idx} ${filename}`);
66
+ const filename = io.chalk.gray(relativeUriFilename(p.filename, cwd));
67
+ return { idx, filename };
73
68
  }
74
- function reportProgressFileComplete(p) {
75
- const time = reportTime(p.elapsedTimeMs, !!p.cached);
69
+ function reportProgressFileBegin(io, p, cwdURL) {
70
+ const { idx, filename } = determineFilename(io, p, cwdURL);
71
+ if (io.getColorLevel() > 0) {
72
+ io.clearLine?.(0);
73
+ io.write(`${idx} ${filename}\r`);
74
+ }
75
+ }
76
+ function reportProgressFileComplete(io, p, cwd, options) {
77
+ const { idx, filename } = determineFilename(io, p, cwd);
78
+ const { verbose, debug } = options;
79
+ const time = reportTime(io, p.elapsedTimeMs, !!p.cached);
76
80
  const skipped = p.processed === false ? ' skipped' : '';
77
- const hasErrors = p.numErrors ? chalk.red ` X` : '';
78
- consoleError(` ${time}${skipped}${hasErrors}`);
81
+ const hasErrors = p.numErrors ? io.chalk.red ` X` : '';
82
+ const newLine = (skipped && (verbose || debug)) || hasErrors || isSlow(p.elapsedTimeMs) || io.getColorLevel() < 1 ? '\n' : '';
83
+ const msg = `${idx} ${filename} ${time}${skipped}${hasErrors}${newLine || '\r'}`;
84
+ io.write(msg);
79
85
  }
80
- function reportTime(elapsedTimeMs, cached) {
86
+ function reportTime(io, elapsedTimeMs, cached) {
81
87
  if (cached)
82
- return chalk.green('cached');
88
+ return io.chalk.green('cached');
83
89
  if (elapsedTimeMs === undefined)
84
90
  return '-';
85
- const color = elapsedTimeMs < 1000 ? chalk.white : elapsedTimeMs < 2000 ? chalk.yellow : chalk.redBright;
91
+ const slow = isSlow(elapsedTimeMs);
92
+ const color = !slow ? io.chalk.white : slow === 1 ? io.chalk.yellow : io.chalk.redBright;
86
93
  return color(elapsedTimeMs.toFixed(2) + 'ms');
87
94
  }
95
+ function isSlow(elapsedTmeMs) {
96
+ if (!elapsedTmeMs || elapsedTmeMs < 1000)
97
+ return 0;
98
+ if (elapsedTmeMs < 2000)
99
+ return 1;
100
+ return 2;
101
+ }
88
102
  export function getReporter(options, config) {
89
103
  const perfStats = {
90
104
  filesProcessed: 0,
@@ -94,7 +108,7 @@ export function getReporter(options, config) {
94
108
  perf: Object.create(null),
95
109
  };
96
110
  const uniqueIssues = config?.unique || false;
97
- const issueTemplate = options.wordsOnly
111
+ const defaultIssueTemplate = options.wordsOnly
98
112
  ? templateIssueWordsOnly
99
113
  : options.legacy
100
114
  ? templateIssueLegacy
@@ -108,23 +122,38 @@ export function getReporter(options, config) {
108
122
  ? templateIssueNoFix
109
123
  : templateIssue;
110
124
  const { fileGlobs, silent, summary, issues, progress: showProgress, verbose, debug } = options;
125
+ const issueTemplate = config?.issueTemplate || defaultIssueTemplate;
126
+ assertCheckTemplate(issueTemplate);
127
+ const console = config?.console || customConsole;
128
+ const stdio = {
129
+ ...console.stdoutChannel,
130
+ chalk: new Chalk({ level: console.stdoutChannel.getColorLevel() }),
131
+ };
132
+ const stderr = {
133
+ ...console.stderrChannel,
134
+ chalk: new Chalk({ level: console.stderrChannel.getColorLevel() }),
135
+ };
136
+ const consoleError = (msg) => stderr.writeLine(msg);
137
+ function createInfoLog(wrap) {
138
+ return (msg) => console.info(wrap(msg));
139
+ }
111
140
  const emitters = {
112
- Debug: !silent && debug ? (s) => console.info(chalk.cyan(s)) : nullEmitter,
113
- Info: !silent && verbose ? (s) => console.info(chalk.yellow(s)) : nullEmitter,
114
- Warning: (s) => console.info(chalk.yellow(s)),
141
+ Debug: !silent && debug ? createInfoLog(stdio.chalk.cyan) : nullEmitter,
142
+ Info: !silent && verbose ? createInfoLog(stdio.chalk.yellow) : nullEmitter,
143
+ Warning: createInfoLog(stdio.chalk.yellow),
115
144
  };
116
145
  function infoEmitter(message, msgType) {
117
146
  emitters[msgType]?.(message);
118
147
  }
119
- const root = URI.file(path.resolve(options.root || process.cwd()));
120
- const fsPathRoot = root.fsPath;
148
+ const rootURL = toFileDirURL(options.root || process.cwd());
121
149
  function relativeIssue(fn) {
122
150
  const fnFilename = options.relative
123
- ? (uri) => relativeUriFilename(uri, fsPathRoot)
124
- : (uri) => URI.parse(uri).fsPath;
151
+ ? (uri) => relativeUriFilename(uri, rootURL)
152
+ : (uri) => toFilePathOrHref(toFileURL(uri, rootURL));
125
153
  return (i) => {
154
+ const fullFilename = i.uri ? toFilePathOrHref(toFileURL(i.uri, rootURL)) : '';
126
155
  const filename = i.uri ? fnFilename(i.uri) : '';
127
- const r = { ...i, filename };
156
+ const r = { ...i, filename, fullFilename };
128
157
  fn(r);
129
158
  };
130
159
  }
@@ -134,7 +163,7 @@ export function getReporter(options, config) {
134
163
  if (isSpellingDictionaryLoadError(error)) {
135
164
  error = error.cause;
136
165
  }
137
- const errorText = format(chalk.red(message), error.toString());
166
+ const errorText = format(stderr.chalk.red(message), error.toString());
138
167
  errorCollection?.push(errorText);
139
168
  consoleError(errorText);
140
169
  }
@@ -144,6 +173,10 @@ export function getReporter(options, config) {
144
173
  }
145
174
  const { files, issues, cachedFiles, filesWithIssues, errors } = result;
146
175
  const numFilesWithIssues = filesWithIssues.size;
176
+ if (stderr.getColorLevel() > 0) {
177
+ stderr.write('\r');
178
+ stderr.clearLine(0);
179
+ }
147
180
  if (issuesCollection?.length || errorCollection?.length) {
148
181
  consoleError('-------------------------------------------');
149
182
  }
@@ -198,14 +231,14 @@ export function getReporter(options, config) {
198
231
  }
199
232
  function progress(p) {
200
233
  if (!silent && showProgress) {
201
- reportProgress(p, fsPathRoot);
234
+ reportProgress(stderr, p, rootURL, options);
202
235
  }
203
236
  if (p.type === 'ProgressFileComplete') {
204
237
  collectPerfStats(p);
205
238
  }
206
239
  }
207
240
  return {
208
- issue: relativeIssue(silent || !issues ? nullEmitter : genIssueEmitter(issueTemplate, uniqueIssues, issuesCollection)),
241
+ issue: relativeIssue(silent || !issues ? nullEmitter : genIssueEmitter(stdio, issueTemplate, uniqueIssues, issuesCollection)),
209
242
  error: silent ? nullEmitter : errorEmitter,
210
243
  info: infoEmitter,
211
244
  debug: emitters.Debug,
@@ -213,7 +246,7 @@ export function getReporter(options, config) {
213
246
  result: !silent && summary ? resultEmitter : nullEmitter,
214
247
  };
215
248
  }
216
- function formatIssue(templateStr, issue, maxIssueTextWidth) {
249
+ function formatIssue(io, templateStr, issue, maxIssueTextWidth) {
217
250
  function clean(t) {
218
251
  return t.replace(/\s+/, ' ');
219
252
  }
@@ -225,9 +258,9 @@ function formatIssue(templateStr, issue, maxIssueTextWidth) {
225
258
  const rowText = row.toString();
226
259
  const colText = col.toString();
227
260
  const padRowCol = ' '.repeat(Math.max(1, 8 - (rowText.length + colText.length)));
228
- const suggestions = formatSuggestions(issue);
261
+ const suggestions = formatSuggestions(io, issue);
229
262
  const msg = issue.message || (issue.isFlagged ? 'Forbidden word' : 'Unknown word');
230
- const message = issue.isFlagged ? `{yellow ${msg}}` : msg;
263
+ const messageColored = issue.isFlagged ? `{yellow ${msg}}` : msg;
231
264
  const substitutions = {
232
265
  $col: colText,
233
266
  $contextFull: contextFull,
@@ -240,16 +273,19 @@ function formatIssue(templateStr, issue, maxIssueTextWidth) {
240
273
  $suggestions: suggestions,
241
274
  $text: text,
242
275
  $uri: uri,
243
- $quickFix: formatQuickFix(issue),
276
+ $quickFix: formatQuickFix(io, issue),
277
+ $message: msg,
278
+ $messageColored: messageColored,
244
279
  };
245
- const t = template(templateStr.replaceAll('$message', message));
280
+ const t = templateStr.replaceAll('$messageColored', messageColored);
281
+ const chalkTemplate = makeTemplate(io.chalk);
246
282
  return substitute(chalkTemplate(t), substitutions).trimEnd();
247
283
  }
248
- function formatSuggestions(issue) {
284
+ function formatSuggestions(io, issue) {
249
285
  if (issue.suggestionsEx) {
250
286
  return issue.suggestionsEx
251
287
  .map((sug) => sug.isPreferred
252
- ? chalk.italic(chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + '*'
288
+ ? io.chalk.italic(io.chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + '*'
253
289
  : sug.wordAdjustedToMatchCase || sug.word)
254
290
  .join(', ');
255
291
  }
@@ -258,7 +294,7 @@ function formatSuggestions(issue) {
258
294
  }
259
295
  return '';
260
296
  }
261
- function formatQuickFix(issue) {
297
+ function formatQuickFix(io, issue) {
262
298
  if (!issue.suggestionsEx?.length)
263
299
  return '';
264
300
  const preferred = issue.suggestionsEx
@@ -266,25 +302,21 @@ function formatQuickFix(issue) {
266
302
  .map((sug) => sug.wordAdjustedToMatchCase || sug.word);
267
303
  if (!preferred.length)
268
304
  return '';
269
- const fixes = preferred.map((w) => chalk.italic(chalk.yellow(w)));
305
+ const fixes = preferred.map((w) => io.chalk.italic(io.chalk.yellow(w)));
270
306
  return `fix: (${fixes.join(', ')})`;
271
307
  }
272
- class TS extends Array {
273
- raw;
274
- constructor(s) {
275
- super(s);
276
- this.raw = [s];
277
- }
278
- }
279
- function template(s) {
280
- return new TS(s);
281
- }
282
308
  function substitute(text, substitutions) {
283
309
  const subs = [];
284
310
  for (const [match, replaceWith] of Object.entries(substitutions)) {
285
311
  const len = match.length;
286
- for (let i = text.indexOf(match); i >= 0; i = text.indexOf(match, i + 1)) {
287
- subs.push([i, i + len, replaceWith]);
312
+ for (let i = text.indexOf(match); i >= 0; i = text.indexOf(match, i)) {
313
+ const end = i + len;
314
+ const reg = /\b/y;
315
+ reg.lastIndex = end;
316
+ if (reg.test(text)) {
317
+ subs.push([i, end, replaceWith]);
318
+ }
319
+ i = end;
288
320
  }
289
321
  }
290
322
  subs.sort((a, b) => a[0] - b[0]);
@@ -298,6 +330,45 @@ function substitute(text, substitutions) {
298
330
  const parts = subs.map(sub);
299
331
  return parts.join('') + text.slice(i);
300
332
  }
333
+ function assertCheckTemplate(template) {
334
+ const r = checkTemplate(template);
335
+ if (r instanceof Error) {
336
+ throw r;
337
+ }
338
+ }
339
+ export function checkTemplate(template) {
340
+ const chalk = new Chalk();
341
+ const chalkTemplate = makeTemplate(chalk);
342
+ const substitutions = {
343
+ $col: '<col>',
344
+ $contextFull: '<contextFull>',
345
+ $contextLeft: '<contextLeft>',
346
+ $contextRight: '<contextRight>',
347
+ $filename: '<filename>',
348
+ $padContext: '<padContext>',
349
+ $padRowCol: '<padRowCol>',
350
+ $row: '<row>',
351
+ $suggestions: '<suggestions>',
352
+ $text: '<text>',
353
+ $uri: '<uri>',
354
+ $quickFix: '<quickFix>',
355
+ $message: '<message>',
356
+ $messageColored: '<messageColored>',
357
+ };
358
+ try {
359
+ const t = chalkTemplate(template);
360
+ const result = substitute(t, substitutions);
361
+ const problems = [...result.matchAll(/\$[a-z]+/gi)].map((m) => m[0]);
362
+ if (problems.length) {
363
+ throw new Error(`Unresolved template variable${problems.length > 1 ? 's' : ''}: ${problems.map((v) => `'${v}'`).join(', ')}`);
364
+ }
365
+ return true;
366
+ }
367
+ catch (e) {
368
+ const msg = e instanceof Error ? e.message : `${e}`;
369
+ return new ApplicationError(msg);
370
+ }
371
+ }
301
372
  export const __testing__ = {
302
373
  formatIssue,
303
374
  };
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { Option as CommanderOption } from 'commander';
3
- import * as App from './application.js';
4
- import { checkText } from './application.js';
3
+ import * as App from './application.mjs';
4
+ import { checkText } from './application.mjs';
5
+ import { console } from './console.js';
5
6
  import { CheckFailed } from './util/errors.js';
6
7
  export function commandCheck(prog) {
7
8
  return prog
@@ -1,3 +1,4 @@
1
+ import { console } from './console.js';
1
2
  import { addPathsToGlobalImports, addPathsToGlobalImportsResultToTable, listGlobalImports, listGlobalImportsResultToTable, removePathsFromGlobalImports, } from './link.js';
2
3
  import { CheckFailed } from './util/errors.js';
3
4
  import { tableToLines } from './util/table.js';
@@ -1,7 +1,8 @@
1
1
  import { Option as CommanderOption } from 'commander';
2
- import * as App from './application.js';
2
+ import * as App from './application.mjs';
3
3
  import { DEFAULT_CACHE_LOCATION } from './util/cache/index.js';
4
4
  import { CheckFailed } from './util/errors.js';
5
+ import { unindent } from './util/unindent.js';
5
6
  // interface InitOptions extends Options {}
6
7
  const usage = `\
7
8
  [options] [globs...] [file://<path> ...] [stdin[://<path>]]
@@ -126,6 +127,7 @@ export function commandLint(prog) {
126
127
  .hideHelp())
127
128
  .addOption(crOpt('--issues-summary-report', 'Output a summary of issues found.').hideHelp())
128
129
  .addOption(crOpt('--show-perf-summary', 'Output a performance summary report.').hideHelp())
130
+ .option('--issue-template [template]', 'Use a custom issue template. See --help --issue-template for details.')
129
131
  // Planned options
130
132
  // .option('--dictionary <dictionary name>', 'Enable a dictionary by name.', collect)
131
133
  // .option('--no-dictionary <dictionary name>', 'Disable a dictionary by name.', collect)
@@ -157,6 +159,38 @@ export function commandLint(prog) {
157
159
  });
158
160
  return spellCheckCommand;
159
161
  }
162
+ function helpIssueTemplate(opts) {
163
+ if (!('issueTemplate' in opts))
164
+ return '';
165
+ return unindent `
166
+ Issue Template:
167
+ Use "--issue-template" to set the template to use when reporting issues.
168
+
169
+ The template is a string that can contain the following placeholders:
170
+ - $filename - the file name
171
+ - $col - the column number
172
+ - $row - the row number
173
+ - $text - the word that is misspelled
174
+ - $message - the issues message: "unknown word", "word is misspelled", etc.
175
+ - $messageColored - the issues message with color based upon the message type.
176
+ - $uri - the URI of the file
177
+ - $suggestions - suggestions for the misspelled word (if requested)
178
+ - $quickFix - possible quick fixes for the misspelled word.
179
+ - $contextFull - the full context of the misspelled word.
180
+ - $contextLeft - the context to the left of the misspelled word.
181
+ - $contextRight - the context to the right of the misspelled word.
182
+
183
+ Color is supported using the following template pattern:
184
+ - \`{<style[.style]> <text>}\` - where \`<style>\` is a style name and \`<text>\` is the text to style.
185
+
186
+ Styles
187
+ - \`bold\`, \`italic\`, \`underline\`, \`strikethrough\`, \`dim\`, \`inverse\`
188
+ - \`black\`, \`red\`, \`green\`, \`yellow\`, \`blue\`, \`magenta\`, \`cyan\`, \`white\`
189
+
190
+ Example:
191
+ --issue-template '{green $filename}:{yellow $row}:{yellow $col} $message {red $text} $quickFix {dim $suggestions}'
192
+ `;
193
+ }
160
194
  /**
161
195
  * Add additional help text to the command.
162
196
  * When the verbose flag is set, show the hidden options.
@@ -166,7 +200,8 @@ export function commandLint(prog) {
166
200
  function augmentCommandHelp(context) {
167
201
  const output = [];
168
202
  const command = context.command;
169
- const showHidden = !!command.opts().verbose;
203
+ const opts = command.opts();
204
+ const showHidden = !!opts.verbose;
170
205
  const hiddenHelp = [];
171
206
  const help = command.createHelp();
172
207
  const hiddenOptions = command.options.filter((opt) => opt.hidden && showHidden);
@@ -179,7 +214,7 @@ function augmentCommandHelp(context) {
179
214
  hiddenHelp.push(help.wrap(` ${options.flags.padEnd(flagColWidth)} ${options.description}`, process.stdout.columns || 80, indent));
180
215
  }
181
216
  output.push(...hiddenHelp, advanced);
182
- return output.join('\n');
217
+ return helpIssueTemplate(opts) + output.join('\n');
183
218
  }
184
219
  /**
185
220
  * Create Option - a helper function to create a commander option.
@@ -1,5 +1,5 @@
1
1
  import { Option as CommanderOption } from 'commander';
2
- import * as App from './application.js';
2
+ import * as App from './application.mjs';
3
3
  import { emitSuggestionResult } from './emitters/suggestionsEmitter.js';
4
4
  import { CheckFailed } from './util/errors.js';
5
5
  function collect(value, previous) {
@@ -1,5 +1,6 @@
1
1
  import { Option as CommanderOption } from 'commander';
2
- import * as App from './application.js';
2
+ import * as App from './application.mjs';
3
+ import { console } from './console.js';
3
4
  import { isDictionaryPathFormat } from './emitters/DictionaryPathFormat.js';
4
5
  import { emitTraceResults } from './emitters/traceEmitter.js';
5
6
  import { CheckFailed } from './util/errors.js';
@@ -0,0 +1,25 @@
1
+ import type { WriteStream } from 'node:tty';
2
+ type Log = (format?: any, ...params: any[]) => void;
3
+ type IOStream = NodeJS.WritableStream & Pick<WriteStream, 'isTTY' | 'rows' | 'columns'> & Pick<Partial<WriteStream>, 'hasColors' | 'clearLine' | 'getColorDepth'>;
4
+ export interface IConsole {
5
+ readonly log: Log;
6
+ readonly error: Log;
7
+ readonly info: Log;
8
+ readonly warn: Log;
9
+ readonly stderrChannel: Channel;
10
+ readonly stdoutChannel: Channel;
11
+ }
12
+ export declare const console: IConsole;
13
+ export declare function log(...p: Parameters<typeof console.log>): void;
14
+ export declare function error(...p: Parameters<typeof console.error>): void;
15
+ export interface Channel {
16
+ stream: IOStream;
17
+ write: (msg: string) => void;
18
+ writeLine: (msg: string) => void;
19
+ clearLine: (dir: -1 | 0 | 1, callback?: () => void) => boolean;
20
+ printLine: (format?: any, ...params: any[]) => void;
21
+ getColorLevel: () => 0 | 1 | 2 | 3;
22
+ }
23
+ export declare function getColorLevel(stream: IOStream): 0 | 1 | 2 | 3;
24
+ export {};
25
+ //# sourceMappingURL=console.d.ts.map