import pico from 'picocolors' import terminalHighlight from './terminal-highlight' /** * The CSS parser throws this error for broken CSS. * * Custom parsers can throw this error for broken custom syntax using * the {@link Node#error} method. * * PostCSS will use the input source map to detect the original error location. * If you wrote a Sass file, compiled it to CSS and then parsed it with PostCSS, * PostCSS will show the original position in the Sass file. * * If you need the position in the PostCSS input * (e.g., to debug the previous compiler), use `error.input.file`. * * @example * // Catching and checking syntax error * try { * postcss.parse('a{') * } catch (error) { * if (error.name === 'CssSyntaxError') { * error //=> CssSyntaxError * } * } * * @example * // Raising error from plugin * throw node.error('Unknown variable', { plugin: 'postcss-vars' }) */ class CssSyntaxError extends Error { /** * @param {string} message Error message. * @param {number} [line] Source line of the error. * @param {number} [column] Source column of the error. * @param {string} [source] Source code of the broken file. * @param {string} [file] Absolute path to the broken file. * @param {string} [plugin] PostCSS plugin name, if error came from plugin. */ constructor (message, line, column, source, file, plugin) { super(message) /** * Always equal to `'CssSyntaxError'`. You should always check error type * by `error.name === 'CssSyntaxError'` * instead of `error instanceof CssSyntaxError`, * because npm could have several PostCSS versions. * * @type {string} * * @example * if (error.name === 'CssSyntaxError') { * error //=> CssSyntaxError * } */ this.name = 'CssSyntaxError' /** * Error message. * * @type {string} * * @example * error.message //=> 'Unclosed block' */ this.reason = message if (file) { /** * Absolute path to the broken file. * * @type {string} * * @example * error.file //=> 'a.sass' * error.input.file //=> 'a.css' */ this.file = file } if (source) { /** * Source code of the broken file. * * @type {string} * * @example * error.source //=> 'a { b {} }' * error.input.column //=> 'a b { }' */ this.source = source } if (plugin) { /** * Plugin name, if error came from plugin. * * @type {string} * * @example * error.plugin //=> 'postcss-vars' */ this.plugin = plugin } if (typeof line !== 'undefined' && typeof column !== 'undefined') { /** * Source line of the error. * * @type {number} * * @example * error.line //=> 2 * error.input.line //=> 4 */ this.line = line /** * Source column of the error. * * @type {number} * * @example * error.column //=> 1 * error.input.column //=> 4 */ this.column = column } this.setMessage() if (Error.captureStackTrace) { Error.captureStackTrace(this, CssSyntaxError) } } setMessage () { /** * Full error text in the GNU error format * with plugin, file, line and column. * * @type {string} * * @example * error.message //=> 'a.css:1:1: Unclosed block' */ this.message = this.plugin ? this.plugin + ': ' : '' this.message += this.file ? this.file : '' if (typeof this.line !== 'undefined') { this.message += ':' + this.line + ':' + this.column } this.message += ': ' + this.reason } /** * Returns a few lines of CSS source that caused the error. * * If the CSS has an input source map without `sourceContent`, * this method will return an empty string. * * @param {boolean} [color] Whether arrow will be colored red by terminal * color codes. By default, PostCSS will detect * color support by `process.stdout.isTTY` * and `process.env.NODE_DISABLE_COLORS`. * * @example * error.showSourceCode() //=> " 4 | } * // 5 | a { * // > 6 | bad * // | ^ * // 7 | } * // 8 | b {" * * @return {string} Few lines of CSS source that caused the error. */ showSourceCode (color) { if (!this.source) return '' let css = this.source if (terminalHighlight) { if (typeof color === 'undefined') color = pico.isColorSupported if (color) css = terminalHighlight(css) } let lines = css.split(/\r?\n/) let start = Math.max(this.line - 3, 0) let end = Math.min(this.line + 2, lines.length) let maxWidth = String(end).length function mark (text) { if (color && pico.red) { return pico.red(pico.bold(text)) } return text } function aside (text) { if (color && pico.gray) { return pico.gray(text) } return text } return lines .slice(start, end) .map((line, index) => { let number = start + 1 + index let gutter = ' ' + (' ' + number).slice(-maxWidth) + ' | ' if (number === this.line) { let spacing = aside(gutter.replace(/\d/g, ' ')) + line.slice(0, this.column - 1).replace(/[^\t]/g, ' ') return mark('>') + aside(gutter) + line + '\n ' + spacing + mark('^') } return ' ' + aside(gutter) + line }) .join('\n') } /** * Returns error position, message and source code of the broken part. * * @example * error.toString() //=> "CssSyntaxError: app.css:1:1: Unclosed block * // > 1 | a { * // | ^" * * @return {string} Error position, message and source code. */ toString () { let code = this.showSourceCode() if (code) { code = '\n\n' + code + '\n' } return this.name + ': ' + this.message + code } /** * @memberof CssSyntaxError# * @member {Input} input Input object with PostCSS internal information * about input file. If input has source map * from previous tool, PostCSS will use origin * (for example, Sass) source. You can use this * object to get PostCSS input source. * * @example * error.input.file //=> 'a.css' * error.file //=> 'a.sass' */ } export default CssSyntaxError