diff --git a/packages/babel-code-frame/README.md b/packages/babel-code-frame/README.md index 7ef5368d31..d54afecb37 100644 --- a/packages/babel-code-frame/README.md +++ b/packages/babel-code-frame/README.md @@ -11,27 +11,54 @@ npm install --save-dev babel-code-frame ## Usage ```js -import codeFrame from 'babel-code-frame'; +import { codeFrameColumns } from 'babel-code-frame'; const rawLines = `class Foo { constructor() }`; -const lineNumber = 2; -const colNumber = 16; +const location = { start: { line: 2, column: 16 } }; -const result = codeFrame(rawLines, lineNumber, colNumber, { /* options */ }); +const result = codeFrameColumns(rawLines, location, { /* options */ }); console.log(result); ``` -```sh +``` 1 | class Foo { > 2 | constructor() | ^ 3 | } ``` -If the column number is not known, you may pass `null` instead. +If the column number is not known, you may omit it. + +You can also pass an `end` hash in `location`. + +```js +import { codeFrameColumns } from 'babel-code-frame'; + +const rawLines = `class Foo { + constructor() { + console.log("hello"); + } +}`; +const location = { start: { line: 2, column: 17 }, end: { line: 4, column: 3 } }; + +const result = codeFrameColumns(rawLines, location, { /* options */ }); + +console.log(result); +``` + +``` + 1 | class Foo { +> 2 | constructor() { + | ^ +> 3 | console.log("hello"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +> 4 | } + | ^^^ + 5 | }; +``` ## Options @@ -58,3 +85,42 @@ Adjust the number of lines to show below the error. `boolean`, defaults to `false`. Enable this to forcibly syntax highlight the code as JavaScript (for non-terminals); overrides `highlightCode`. + +## Upgrading from prior versions + +Prior to version 7, the only API exposed by this module was for a single line and optional column pointer. The old API will now log a deprecation warning. + +The new API takes a `location` object, similar to what is available in an AST. + +This is an example of the deprecated (but still available) API: + +```js +import codeFrame from 'babel-code-frame'; + +const rawLines = `class Foo { + constructor() +}`; +const lineNumber = 2; +const colNumber = 16; + +const result = codeFrame(rawLines, lineNumber, colNumber, { /* options */ }); + +console.log(result); +``` + +To get the same highlighting using the new API: + +```js +import { codeFrameColumns } from 'babel-code-frame'; + +const rawLines = `class Foo { + constructor() { + console.log("hello"); + } +}`; +const location = { start: { line: 2, column: 16 } }; + +const result = codeFrameColumns(rawLines, location, { /* options */ }); + +console.log(result); +``` diff --git a/packages/babel-code-frame/src/index.js b/packages/babel-code-frame/src/index.js index 5dbf1b8ada..f8564a0a23 100644 --- a/packages/babel-code-frame/src/index.js +++ b/packages/babel-code-frame/src/index.js @@ -2,6 +2,18 @@ import jsTokens, { matchToToken } from "js-tokens"; import esutils from "esutils"; import Chalk from "chalk"; +let deprecationWarningShown = false; + +type Location = { + column: number, + line: number, +}; + +type NodeLocation = { + end: Location, + start: Location, +}; + /** * Chalk styles for token types. */ @@ -90,17 +102,74 @@ function highlight(defs: Object, text: string) { } /** - * Create a code frame, adding line numbers, code highlighting, and pointing to a given position. + * Extract what lines should be marked and highlighted. */ -export default function ( +function getMarkerLines( + loc: NodeLocation, source: Array, opts: Object +): { start: number, end: number, markerLines: Object } { + const startLoc: Location = Object.assign({}, { column: 0, line: -1 }, loc.start); + const endLoc: Location = Object.assign({}, startLoc, loc.end); + const linesAbove = opts.linesAbove || 2; + const linesBelow = opts.linesBelow || 3; + + const startLine = startLoc.line; + const startColumn = startLoc.column; + const endLine = endLoc.line; + const endColumn = endLoc.column; + + let start = Math.max(startLine - (linesAbove + 1), 0); + let end = Math.min(source.length, endLine + linesBelow); + + if (startLine === -1) { + start = 0; + } + + if (endLine === -1) { + end = source.length; + } + + const lineDiff = endLine - startLine; + const markerLines = {}; + + if (lineDiff) { + for (let i = 0; i <= lineDiff; i++) { + const lineNumber = i + startLine; + + if (!startColumn) { + markerLines[lineNumber] = true; + } else if (i === 0) { + const sourceLength = source[lineNumber - 1].length; + + markerLines[lineNumber] = [startColumn, sourceLength - startColumn]; + } else if (i === lineDiff) { + markerLines[lineNumber] = [0, endColumn]; + } else { + const sourceLength = source[lineNumber - i].length; + + markerLines[lineNumber] = [0, sourceLength]; + } + } + } else { + if (startColumn === endColumn) { + if (startColumn) { + markerLines[startLine] = [startColumn, 0]; + } else { + markerLines[startLine] = true; + } + } else { + markerLines[startLine] = [startColumn, endColumn - startColumn]; + } + } + + return { start, end, markerLines }; +} + +export function codeFrameColumns ( rawLines: string, - lineNumber: number, - colNumber: ?number, + loc: NodeLocation, opts: Object = {}, ): string { - colNumber = Math.max(colNumber, 0); - const highlighted = (opts.highlightCode && Chalk.supportsColor) || opts.forceColor; let chalk = Chalk; if (opts.forceColor) { @@ -112,17 +181,8 @@ export default function ( const defs = getDefs(chalk); if (highlighted) rawLines = highlight(defs, rawLines); - const linesAbove = opts.linesAbove || 2; - const linesBelow = opts.linesBelow || 3; - const lines = rawLines.split(NEWLINE); - let start = Math.max(lineNumber - (linesAbove + 1), 0); - let end = Math.min(lines.length, lineNumber + linesBelow); - - if (!lineNumber && !colNumber) { - start = 0; - end = lines.length; - } + const { start, end, markerLines } = getMarkerLines(loc, lines, opts); const numberMaxWidth = String(end).length; @@ -130,15 +190,18 @@ export default function ( const number = start + 1 + index; const paddedNumber = ` ${number}`.slice(-numberMaxWidth); const gutter = ` ${paddedNumber} | `; - if (number === lineNumber) { + const hasMarker = markerLines[number]; + if (hasMarker) { let markerLine = ""; - if (colNumber) { - const markerSpacing = line.slice(0, colNumber - 1).replace(/[^\t]/g, " "); + if (Array.isArray(hasMarker)) { + const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " "); + const numberOfMarkers = hasMarker[1] || 1; + markerLine = [ "\n ", maybeHighlight(defs.gutter, gutter.replace(/\d/g, " ")), markerSpacing, - maybeHighlight(defs.marker, "^"), + maybeHighlight(defs.marker, "^").repeat(numberOfMarkers), ].join(""); } return [ @@ -158,3 +221,35 @@ export default function ( return frame; } } + +/** + * Create a code frame, adding line numbers, code highlighting, and pointing to a given position. + */ + +export default function ( + rawLines: string, + lineNumber: number, + colNumber: ?number, + opts: Object = {}, +): string { + if (!deprecationWarningShown) { + deprecationWarningShown = true; + + const deprecationError = new Error( + "Passing lineNumber and colNumber is deprecated to babel-code-frame. Please use `codeFrameColumns`." + ); + deprecationError.name = "DeprecationWarning"; + + if (process.emitWarning) { + process.emitWarning(deprecationError); + } else { + console.warn(deprecationError); + } + } + + colNumber = Math.max(colNumber, 0); + + const location: NodeLocation = { start: { column: colNumber, line: lineNumber } }; + + return codeFrameColumns(rawLines, location, opts); +} diff --git a/packages/babel-code-frame/test/index.js b/packages/babel-code-frame/test/index.js index fa1455c496..a0a1223463 100644 --- a/packages/babel-code-frame/test/index.js +++ b/packages/babel-code-frame/test/index.js @@ -1,6 +1,6 @@ import assert from "assert"; import chalk from "chalk"; -import codeFrame from ".."; +import codeFrame, { codeFrameColumns } from ".."; describe("babel-code-frame", function () { it("basic usage", function () { @@ -30,19 +30,6 @@ describe("babel-code-frame", function () { ].join("\n")); }); - it("optional column number", function () { - const rawLines = [ - "class Foo {", - " constructor()", - "};", - ].join("\n"); - assert.equal(codeFrame(rawLines, 2, null), [ - " 1 | class Foo {", - "> 2 | constructor()", - " 3 | };", - ].join("\n")); - }); - it("maximum context lines and padding", function () { const rawLines = [ "/**", @@ -205,4 +192,90 @@ describe("babel-code-frame", function () { ].join("\n")) ); }); + + it("basic usage, new API", function () { + const rawLines = [ + "class Foo {", + " constructor()", + "};", + ].join("\n"); + assert.equal(codeFrameColumns(rawLines, { start: { line: 2, column: 16 } }), [ + " 1 | class Foo {", + "> 2 | constructor()", + " | ^", + " 3 | };", + ].join("\n")); + }); + + it("mark multiple columns", function() { + const rawLines = [ + "class Foo {", + " constructor()", + "};", + ].join("\n"); + assert.equal( + codeFrameColumns(rawLines, { start: { line: 2, column: 3 }, end: { line: 2, column: 16 } }), [ + " 1 | class Foo {", + "> 2 | constructor()", + " | ^^^^^^^^^^^^^", + " 3 | };", + ].join("\n")); + }); + + it("mark multiple columns across lines", function() { + const rawLines = [ + "class Foo {", + " constructor() {", + " }", + "};", + ].join("\n"); + assert.equal( + codeFrameColumns(rawLines, { start: { line: 2, column: 17 }, end: { line: 3, column: 3 } }), [ + " 1 | class Foo {", + "> 2 | constructor() {", + " | ^", + "> 3 | }", + " | ^^^", + " 4 | };", + ].join("\n")); + }); + + it("mark multiple columns across multiple lines", function() { + const rawLines = [ + "class Foo {", + " constructor() {", + " console.log(arguments);", + " }", + "};", + ].join("\n"); + assert.equal( + codeFrameColumns(rawLines, { start: { line: 2, column: 17 }, end: { line: 4, column: 3 } }), [ + " 1 | class Foo {", + "> 2 | constructor() {", + " | ^", + "> 3 | console.log(arguments);", + " | ^^^^^^^^^^^^^^^^^^^^^^^^^^^", + "> 4 | }", + " | ^^^", + " 5 | };", + ].join("\n")); + }); + + it("mark across multiple lines without columns", function() { + const rawLines = [ + "class Foo {", + " constructor() {", + " console.log(arguments);", + " }", + "};", + ].join("\n"); + assert.equal( + codeFrameColumns(rawLines, { start: { line: 2 }, end: { line: 4 } }), [ + " 1 | class Foo {", + "> 2 | constructor() {", + "> 3 | console.log(arguments);", + "> 4 | }", + " 5 | };", + ].join("\n")); + }); });