4
0

stringFormatter.cjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // NOTICE: This file is generated by Rollup. To modify it,
  2. // please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
  3. 'use strict';
  4. const node_path = require('node:path');
  5. const process = require('node:process');
  6. const table = require('table');
  7. const picocolors = require('picocolors');
  8. const stringWidth = require('string-width');
  9. const validateTypes = require('../utils/validateTypes.cjs');
  10. const calcSeverityCounts = require('./calcSeverityCounts.cjs');
  11. const isUnicodeSupported = require('../utils/isUnicodeSupported.cjs');
  12. const pluralize = require('../utils/pluralize.cjs');
  13. const preprocessWarnings = require('./preprocessWarnings.cjs');
  14. const terminalLink = require('./terminalLink.cjs');
  15. const { yellow, dim, underline, blue, red, green } = picocolors;
  16. const NON_ASCII_PATTERN = /\P{ASCII}/u;
  17. const MARGIN_WIDTHS = 9;
  18. /**
  19. * @param {string} s
  20. * @returns {string}
  21. */
  22. function identity(s) {
  23. return s;
  24. }
  25. const levelColors = {
  26. info: blue,
  27. warning: yellow,
  28. error: red,
  29. success: identity,
  30. };
  31. const supportsUnicode = isUnicodeSupported();
  32. const symbols = {
  33. info: blue(supportsUnicode ? 'ℹ' : 'i'),
  34. warning: yellow(supportsUnicode ? '⚠' : '‼'),
  35. error: red(supportsUnicode ? '✖' : '×'),
  36. success: green(supportsUnicode ? '✔' : '√'),
  37. };
  38. /**
  39. * @param {import('stylelint').LintResult[]} results
  40. * @returns {string}
  41. */
  42. function deprecationsFormatter(results) {
  43. const allDeprecationWarnings = results.flatMap((result) => result.deprecations || []);
  44. if (allDeprecationWarnings.length === 0) {
  45. return '';
  46. }
  47. const seenText = new Set();
  48. const lines = [];
  49. for (const { text, reference } of allDeprecationWarnings) {
  50. if (seenText.has(text)) continue;
  51. seenText.add(text);
  52. let line = ` ${dim('-')} ${text}`;
  53. if (reference) {
  54. line += dim(` See: ${underline(reference)}`);
  55. }
  56. lines.push(line);
  57. }
  58. return ['', yellow('Deprecation warnings:'), ...lines, ''].join('\n');
  59. }
  60. /**
  61. * @param {import('stylelint').LintResult[]} results
  62. * @returns {string}
  63. */
  64. function invalidOptionsFormatter(results) {
  65. const allInvalidOptionWarnings = results.flatMap((result) =>
  66. (result.invalidOptionWarnings || []).map((warning) => warning.text),
  67. );
  68. const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)];
  69. return uniqueInvalidOptionWarnings.reduce((output, warning) => {
  70. output += red('Invalid Option: ');
  71. output += warning;
  72. return `${output}\n`;
  73. }, '\n');
  74. }
  75. /**
  76. * @param {string} fromValue
  77. * @param {string} cwd
  78. * @returns {string}
  79. */
  80. function logFrom(fromValue, cwd) {
  81. if (fromValue.startsWith('<')) {
  82. return underline(fromValue);
  83. }
  84. const filePath = node_path.relative(cwd, fromValue).split(node_path.sep).join('/');
  85. return terminalLink(filePath, `file://${fromValue}`);
  86. }
  87. /**
  88. * @param {{[k: number]: number}} columnWidths
  89. * @returns {number}
  90. */
  91. function getMessageWidth(columnWidths) {
  92. const width = columnWidths[3];
  93. validateTypes.assertNumber(width);
  94. if (!process.stdout.isTTY) {
  95. return width;
  96. }
  97. const availableWidth = process.stdout.columns < 80 ? 80 : process.stdout.columns;
  98. const fullWidth = Object.values(columnWidths).reduce((a, b) => a + b);
  99. // If there is no reason to wrap the text, we won't align the last column to the right
  100. if (availableWidth > fullWidth + MARGIN_WIDTHS) {
  101. return width;
  102. }
  103. return availableWidth - (fullWidth - width + MARGIN_WIDTHS);
  104. }
  105. /**
  106. * @param {import('stylelint').Warning[]} messages
  107. * @param {string} source
  108. * @param {string} cwd
  109. * @returns {string}
  110. */
  111. function formatter(messages, source, cwd) {
  112. if (messages.length === 0) return '';
  113. /**
  114. * Create a list of column widths, needed to calculate
  115. * the size of the message column and if needed wrap it.
  116. * @type {{[k: string]: number}}
  117. */
  118. const columnWidths = { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 };
  119. /**
  120. * @param {[string, string, string, string, string]} columns
  121. * @returns {[string, string, string, string, string]}
  122. */
  123. function calculateWidths(columns) {
  124. for (const [key, value] of Object.entries(columns)) {
  125. const normalisedValue = value ? value.toString() : value;
  126. const width = columnWidths[key];
  127. validateTypes.assertNumber(width);
  128. columnWidths[key] = Math.max(width, stringWidth(normalisedValue));
  129. }
  130. return columns;
  131. }
  132. let output = '\n';
  133. if (source) {
  134. output += `${logFrom(source, cwd)}\n`;
  135. }
  136. /**
  137. * @param {import('stylelint').Warning} message
  138. * @returns {string}
  139. */
  140. function formatMessageText(message) {
  141. let result = message.text;
  142. result = result
  143. // Remove all control characters (newline, tab and etc)
  144. .replace(/[\u0001-\u001A]+/g, ' ') // eslint-disable-line no-control-regex
  145. .replace(/\.$/, '');
  146. const ruleString = ` (${message.rule})`;
  147. if (result.endsWith(ruleString)) {
  148. result = result.slice(0, result.lastIndexOf(ruleString));
  149. }
  150. return result;
  151. }
  152. const cleanedMessages = messages.map((message) => {
  153. const { line, column, severity } = message;
  154. /**
  155. * @type {[string, string, string, string, string]}
  156. */
  157. const row = [
  158. line ? line.toString() : '',
  159. column ? column.toString() : '',
  160. symbols[severity] ? levelColors[severity](symbols[severity]) : severity,
  161. formatMessageText(message),
  162. dim(message.rule || ''),
  163. ];
  164. calculateWidths(row);
  165. return row;
  166. });
  167. const messageWidth = getMessageWidth(columnWidths);
  168. const hasNonAsciiChar = messages.some((msg) => NON_ASCII_PATTERN.test(msg.text));
  169. output += table.table(cleanedMessages, {
  170. border: table.getBorderCharacters('void'),
  171. columns: {
  172. 0: { alignment: 'right', width: columnWidths[0], paddingRight: 0, paddingLeft: 2 },
  173. 1: { alignment: 'left', width: columnWidths[1] },
  174. 2: { alignment: 'center', width: columnWidths[2] },
  175. 3: {
  176. alignment: 'left',
  177. width: messageWidth,
  178. wrapWord: messageWidth > 1 && !hasNonAsciiChar,
  179. },
  180. 4: { alignment: 'left', width: columnWidths[4], paddingRight: 0 },
  181. },
  182. drawHorizontalLine: () => false,
  183. })
  184. .split('\n')
  185. .map((el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)).trimEnd())
  186. .join('\n');
  187. return output;
  188. }
  189. /**
  190. * @type {import('stylelint').Formatter}
  191. */
  192. function stringFormatter(results, returnValue) {
  193. let output = invalidOptionsFormatter(results);
  194. output += deprecationsFormatter(results);
  195. const resultCounts = { error: 0, warning: 0 };
  196. const fixableCounts = { error: 0, warning: 0 };
  197. output = results.reduce((accum, result) => {
  198. preprocessWarnings(result);
  199. accum += formatter(
  200. result.warnings,
  201. result.source || '',
  202. (returnValue && returnValue.cwd) || process.cwd(),
  203. );
  204. for (const warning of result.warnings) {
  205. calcSeverityCounts(warning.severity, resultCounts);
  206. const fixable = returnValue.ruleMetadata?.[warning.rule]?.fixable;
  207. if (fixable === true) {
  208. calcSeverityCounts(warning.severity, fixableCounts);
  209. }
  210. }
  211. return accum;
  212. }, output);
  213. // Ensure consistent padding
  214. output = output.trim();
  215. if (output !== '') {
  216. output = `\n${output}\n`;
  217. const errorCount = resultCounts.error;
  218. const warningCount = resultCounts.warning;
  219. const total = errorCount + warningCount;
  220. if (total > 0) {
  221. const error = red(`${errorCount} ${pluralize('error', errorCount)}`);
  222. const warning = yellow(`${warningCount} ${pluralize('warning', warningCount)}`);
  223. const symbol = errorCount > 0 ? symbols.error : symbols.warning;
  224. output += `\n${symbol} ${total} ${pluralize('problem', total)} (${error}, ${warning})`;
  225. }
  226. const fixErrorCount = fixableCounts.error;
  227. const fixWarningCount = fixableCounts.warning;
  228. if (fixErrorCount > 0 || fixWarningCount > 0) {
  229. let fixErrorText;
  230. let fixWarningText;
  231. if (fixErrorCount > 0) {
  232. fixErrorText = `${fixErrorCount} ${pluralize('error', fixErrorCount)}`;
  233. }
  234. if (fixWarningCount > 0) {
  235. fixWarningText = `${fixWarningCount} ${pluralize('warning', fixWarningCount)}`;
  236. }
  237. const countText = [fixErrorText, fixWarningText].filter(Boolean).join(' and ');
  238. output += `\n ${countText} potentially fixable with the "--fix" option.`;
  239. }
  240. output += '\n\n';
  241. }
  242. return output;
  243. }
  244. module.exports = stringFormatter;