diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..fb63387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,15 +7,15 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: node: - - lts/erbium + - lts/gallium - node diff --git a/.npmrc b/.npmrc index 43c97e7..3757b30 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +ignore-scripts=true package-lock=false diff --git a/index.js b/index.js index 1b4a320..93163ea 100644 --- a/index.js +++ b/index.js @@ -1,61 +1 @@ -/** - * @typedef {import('unist').Point} Point - * @typedef {import('unist').Position} Position - * @typedef {Record & {type: string, position?: Position|undefined}} NodeLike - */ - -/** - * Stringify one point, a position (start and end points), or a node’s - * positional information. - * - * @param {NodeLike|Position|Point|null} [value] - * @returns {string} - */ -export function stringifyPosition(value) { - // Nothing. - if (!value || typeof value !== 'object') { - return '' - } - - // Node. - if ('position' in value || 'type' in value) { - return position(value.position) - } - - // Position. - if ('start' in value || 'end' in value) { - return position(value) - } - - // Point. - if ('line' in value || 'column' in value) { - return point(value) - } - - // ? - return '' -} - -/** - * @param {Point|undefined} point - * @returns {string} - */ -function point(point) { - return index(point && point.line) + ':' + index(point && point.column) -} - -/** - * @param {Position|undefined} pos - * @returns {string} - */ -function position(pos) { - return point(pos && pos.start) + '-' + point(pos && pos.end) -} - -/** - * @param {number|undefined} value - * @returns {number} - */ -function index(value) { - return value && typeof value === 'number' ? value : 1 -} +export {stringifyPosition} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..109b646 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,84 @@ +/** + * @typedef {import('unist').Node} Node + * @typedef {import('unist').Point} Point + * @typedef {import('unist').Position} Position + */ + +/** + * @typedef NodeLike + * @property {string} type + * @property {PositionLike | null | undefined} [position] + * + * @typedef PointLike + * @property {number | null | undefined} [line] + * @property {number | null | undefined} [column] + * @property {number | null | undefined} [offset] + * + * @typedef PositionLike + * @property {PointLike | null | undefined} [start] + * @property {PointLike | null | undefined} [end] + */ + +/** + * Serialize the positional info of a point, position (start and end points), + * or node. + * + * @param {Node | NodeLike | Point | PointLike | Position | PositionLike | null | undefined} [value] + * Node, position, or point. + * @returns {string} + * Pretty printed positional info of a node (`string`). + * + * In the format of a range `ls:cs-le:ce` (when given `node` or `position`) + * or a point `l:c` (when given `point`), where `l` stands for line, `c` for + * column, `s` for `start`, and `e` for end. + * An empty string (`''`) is returned if the given value is neither `node`, + * `position`, nor `point`. + */ +export function stringifyPosition(value) { + // Nothing. + if (!value || typeof value !== 'object') { + return '' + } + + // Node. + if ('position' in value || 'type' in value) { + return position(value.position) + } + + // Position. + if ('start' in value || 'end' in value) { + return position(value) + } + + // Point. + if ('line' in value || 'column' in value) { + return point(value) + } + + // ? + return '' +} + +/** + * @param {Point | PointLike | null | undefined} point + * @returns {string} + */ +function point(point) { + return index(point && point.line) + ':' + index(point && point.column) +} + +/** + * @param {Position | PositionLike | null | undefined} pos + * @returns {string} + */ +function position(pos) { + return point(pos && pos.start) + '-' + point(pos && pos.end) +} + +/** + * @param {number | null | undefined} value + * @returns {number} + */ +function index(value) { + return value && typeof value === 'number' ? value : 1 +} diff --git a/package.json b/package.json index 38ea1f3..334550f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unist-util-stringify-position", - "version": "3.0.1", + "version": "4.0.0", "description": "unist utility to serialize a node, position, or point as a human readable location", "license": "MIT", "keywords": [ @@ -27,54 +27,54 @@ ], "sideEffects": false, "type": "module", - "main": "index.js", - "types": "index.d.ts", + "exports": "./index.js", "files": [ + "lib/", "index.d.ts", "index.js" ], "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "devDependencies": { - "@types/tape": "^4.0.0", - "c8": "^7.0.0", + "@types/mdast": "^4.0.0", + "@types/node": "^20.0.0", + "c8": "^8.0.0", "prettier": "^2.0.0", - "remark-cli": "^10.0.0", + "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "rimraf": "^3.0.0", - "tape": "^5.0.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", - "xo": "^0.48.0" + "typescript": "^5.0.0", + "xo": "^0.54.0" }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"*.d.ts\" && tsc && type-coverage", + "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", + "test-api": "node --conditions development test.js", + "test-coverage": "c8 --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, "bracketSpacing": false, "semi": false, - "trailingComma": "none" - }, - "xo": { - "prettier": true + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "remark-preset-wooorm" ] }, "typeCoverage": { "atLeast": 100, "detail": true, + "ignoreCatch": true, "strict": true + }, + "xo": { + "prettier": true } } diff --git a/readme.md b/readme.md index 5fe2e1d..af692c8 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -**[unist][]** utility to pretty print the positional information of a node. +[unist][] utility to pretty print the positional info of a node. ## Contents @@ -39,7 +39,7 @@ For example, when throwing errors or warning messages about something. ## Install This package is [ESM only][esm]. -In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][]: ```sh npm install unist-util-stringify-position @@ -48,14 +48,14 @@ npm install unist-util-stringify-position In Deno with [`esm.sh`][esmsh]: ```js -import {stringifyPosition} from 'https://esm.sh/unist-util-stringify-position@3' +import {stringifyPosition} from 'https://esm.sh/unist-util-stringify-position@4' ``` In browsers with [`esm.sh`][esmsh]: ```html ``` @@ -78,41 +78,47 @@ stringifyPosition({ ## API -This package exports the identifier `stringifyPosition`. +This package exports the identifier [`stringifyPosition`][stringifyposition]. There is no default export. ### `stringifyPosition(node|position|point)` -Stringify a [point][], [position][], or a [node][]. +Serialize the positional info of a point, position (start and end points), or +node. ###### Parameters * `node` ([`Node`][node]) - — node whose `'position'` property to stringify + — node whose `position` fields to serialize * `position` ([`Position`][position]) - — position whose `'start'` and `'end'` points to stringify + — position whose `start` and `end` points to serialize * `point` ([`Point`][point]) - — point whose `'line'` and `'column'` to stringify + — point whose `line` and `column` fields to serialize ###### Returns -`string?` — A range `ls:cs-le:ce` (when given `node` or `position`) or a point -`l:c` (when given `point`), where `l` stands for line, `c` for column, `s` for -`start`, and `e` for end. +Pretty printed positional info of a node (`string`). + +In the format of a range `ls:cs-le:ce` (when given `node` or `position`) or a +point `l:c` (when given `point`), where `l` stands for line, `c` for column, `s` +for `start`, and `e` for end. An empty string (`''`) is returned if the given value is neither `node`, `position`, nor `point`. ## Types This package is fully typed with [TypeScript][]. -There are no additional types exported. +It exports no additional types. ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 12.20+, 14.14+, and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +When we cut a new major release, we drop support for unmaintained versions of +Node. +This means we try to keep the current release line, +`unist-util-stringify-position@^4`, compatible with Node.js 16. ## Security @@ -157,9 +163,9 @@ abide by its terms. [downloads]: https://www.npmjs.com/package/unist-util-stringify-position -[size-badge]: https://img.shields.io/bundlephobia/minzip/unist-util-stringify-position.svg +[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=unist-util-stringify-position -[size]: https://bundlephobia.com/result?p=unist-util-stringify-position +[size]: https://bundlejs.com/?q=unist-util-stringify-position [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg @@ -183,11 +189,11 @@ abide by its terms. [typescript]: https://www.typescriptlang.org -[contributing]: https://github.com/syntax-tree/.github/blob/HEAD/contributing.md +[contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md -[support]: https://github.com/syntax-tree/.github/blob/HEAD/support.md +[support]: https://github.com/syntax-tree/.github/blob/main/support.md -[coc]: https://github.com/syntax-tree/.github/blob/HEAD/code-of-conduct.md +[coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md [unist]: https://github.com/syntax-tree/unist @@ -196,3 +202,5 @@ abide by its terms. [position]: https://github.com/syntax-tree/unist#position [point]: https://github.com/syntax-tree/unist#point + +[stringifyposition]: #stringifypositionnodepositionpoint diff --git a/test.js b/test.js index 5ae7b4d..b99f73f 100644 --- a/test.js +++ b/test.js @@ -1,158 +1,212 @@ -import test from 'tape' -import {stringifyPosition} from './index.js' - -test('stringifyPosition', function (t) { - t.equal( - stringifyPosition(), - '', - 'should return empty `string` with `undefined`' - ) - t.equal( - stringifyPosition(null), - '', - 'should return empty `string` with `null`' - ) - t.equal( - // @ts-expect-error runtime. - stringifyPosition('foo'), - '', - 'should return empty `string` with `string`' - ) - t.equal( - // @ts-expect-error runtime. - stringifyPosition(5), - '', - 'should return empty `string` with `number`' - ) - t.equal( - // @ts-expect-error runtime. - stringifyPosition({}), - '', - 'should return empty `string` with `{}`' - ) - - t.equal( - stringifyPosition({type: 'text'}), - '1:1-1:1', - 'should return a range for a `node` without `position`' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({type: 'text', position: 3}), - '1:1-1:1', - 'should return a range for `node` with invalid `position` #1' - ) - - t.equal( - stringifyPosition({ - type: 'text', +import assert from 'node:assert/strict' +import test from 'node:test' +import {stringifyPosition} from 'unist-util-stringify-position' + +test('stringifyPosition', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual( + Object.keys(await import('unist-util-stringify-position')).sort(), + ['stringifyPosition'] + ) + }) + + await t.test( + 'should return empty `string` with `undefined`', + async function () { + assert.equal(stringifyPosition(), '') + } + ) + + await t.test('should return empty `string` with `null`', async function () { + assert.equal(stringifyPosition(null), '') + }) + + await t.test('should return empty `string` with `string`', async function () { + assert.equal( // @ts-expect-error runtime. - position: {start: {}, end: {}} - }), - '1:1-1:1', - 'should return a range for `node` with invalid `position` #2' - ) + stringifyPosition('foo'), + '' + ) + }) - t.equal( - stringifyPosition({ - type: 'text', - position: { + await t.test('should return empty `string` with `number`', async function () { + assert.equal( + // @ts-expect-error runtime. + stringifyPosition(5), + '' + ) + }) + + await t.test('should return empty `string` with `{}`', async function () { + assert.equal(stringifyPosition({}), '') + }) + + await t.test( + 'should return a range for a `node` without `position`', + async function () { + assert.equal(stringifyPosition({type: 'text'}), '1:1-1:1') + } + ) + + await t.test( + 'should return a range for `node` with invalid `position` #1', + async function () { + assert.equal( // @ts-expect-error runtime. - start: {line: null, column: null}, + stringifyPosition({type: 'text', position: 3}), + '1:1-1:1' + ) + } + ) + + await t.test( + 'should return a range for `node` with invalid `position` #2', + async function () { + assert.equal( + stringifyPosition({ + type: 'text', + position: {start: {}, end: {}} + }), + '1:1-1:1' + ) + } + ) + + await t.test( + 'should return a range for `node` with invalid `position` #3', + async function () { + assert.equal( + stringifyPosition({ + type: 'text', + position: { + start: {line: null, column: null}, + end: {line: null, column: null} + } + }), + '1:1-1:1' + ) + } + ) + + await t.test( + 'should return a range for `node` with valid `position` (types: literal object)', + async function () { + assert.equal( + stringifyPosition({ + type: 'text', + position: { + start: {line: 2, column: 5}, + end: {line: 2, column: 6} + } + }), + '2:5-2:6' + ) + } + ) + + await t.test( + 'should return a range for `node` with valid `position` (types: explicit instance of node)', + async function () { + assert.equal( + stringifyPosition( + /** @type {import('mdast').Root} */ ({ + type: 'root', + children: [], + position: { + start: {line: 1, column: 1}, + end: {line: 2, column: 1} + } + }) + ), + '1:1-2:1' + ) + } + ) + + await t.test( + 'should return a range for a `position` without `point`s', + async function () { + assert.equal(stringifyPosition({start: null, end: null}), '1:1-1:1') + } + ) + + await t.test( + 'should return a range for `position` with invalid `point`s #1', + async function () { + assert.equal( // @ts-expect-error runtime. - end: {line: null, column: null} - } - }), - '1:1-1:1', - 'should return a range for `node` with invalid `position` #3' - ) - - t.equal( - stringifyPosition({ - type: 'text', - position: { - start: {line: 2, column: 5}, - end: {line: 2, column: 6} - } - }), - '2:5-2:6', - 'should return a range for `node` with valid `position`' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({start: null, end: null}), - '1:1-1:1', - 'should return a range for a `position` without `point`s' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({start: 3, end: 6}), - '1:1-1:1', - 'should return a range for `position` with invalid `point`s #1' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({start: {}, end: {}}), - '1:1-1:1', - 'should return range for `position` with invalid `point`s #1' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({ - start: {line: null, column: null}, - end: {line: null, column: null} - }), - '1:1-1:1', - 'should return range for `position` with invalid `point`s #3' - ) - - t.equal( - stringifyPosition({ - start: {line: 2, column: 5}, - end: {line: 2, column: 6} - }), - '2:5-2:6', - 'should return range for `position` with valid `point`s' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({line: null, column: null}), - '1:1', - 'should return a point for a `point` without indices' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({line: 'foo', column: 'bar'}), - '1:1', - 'should return a point for a `point` with invalid indices #1' - ) - - t.equal( - // @ts-expect-error runtime. - stringifyPosition({line: 4}), - '4:1', - 'should return a point for a partially valid `point` #1' + stringifyPosition({start: 3, end: 6}), + '1:1-1:1' + ) + } + ) + + await t.test( + 'should return range for `position` with invalid `point`s #1', + async function () { + assert.equal(stringifyPosition({start: {}, end: {}}), '1:1-1:1') + } + ) + + await t.test( + 'should return range for `position` with invalid `point`s #3', + async function () { + assert.equal( + stringifyPosition({ + start: {line: null, column: null}, + end: {line: null, column: null} + }), + '1:1-1:1' + ) + } + ) + + await t.test( + 'should return range for `position` with valid `point`s', + async function () { + assert.equal( + stringifyPosition({ + start: {line: 2, column: 5}, + end: {line: 2, column: 6} + }), + '2:5-2:6' + ) + } + ) + + await t.test( + 'should return a point for a `point` without indices', + async function () { + assert.equal(stringifyPosition({line: null, column: null}), '1:1') + } + ) + + await t.test( + 'should return a point for a `point` with invalid indices #1', + async function () { + assert.equal( + // @ts-expect-error runtime. + stringifyPosition({line: 'foo', column: 'bar'}), + '1:1' + ) + } ) - t.equal( - // @ts-expect-error runtime. - stringifyPosition({column: 12}), - '1:12', - 'should return a point for a partially valid `point` #1' + await t.test( + 'should return a point for a partially valid `point` #1', + async function () { + assert.equal(stringifyPosition({line: 4}), '4:1') + } ) - t.equal( - stringifyPosition({line: 5, column: 2}), - '5:2', - 'should return a point for a valid `point`' + await t.test( + 'should return a point for a partially valid `point` #1', + async function () { + assert.equal(stringifyPosition({column: 12}), '1:12') + } ) - t.end() + await t.test('should return a point for a valid `point`', async function () { + assert.equal(stringifyPosition({line: 5, column: 2}), '5:2') + }) }) diff --git a/tsconfig.json b/tsconfig.json index e31adf8..82cc749 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,15 @@ { - "include": ["*.js"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "ES2020", - "moduleResolution": "node", - "allowJs": true, "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "strict": true - } + "exactOptionalPropertyTypes": true, + "lib": ["es2022"], + "module": "node16", + "strict": true, + "target": "es2022" + }, + "exclude": ["coverage/", "node_modules/"], + "include": ["**/*.js"] }