stringFormatter.js 6.2 KB

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