Skip to content

Commit

Permalink
feat: Location
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
  • Loading branch information
unicornware committed May 6, 2024
1 parent 366deb3 commit 97c894b
Show file tree
Hide file tree
Showing 19 changed files with 491 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .attw.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"color": true,
"emoji": true,
"format": "ascii",
"ignoreRules": ["cjs-resolves-to-esm", "no-resolution"],
"ignoreRules": ["cjs-resolves-to-esm"],
"summary": true
}
3 changes: 2 additions & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ ignore:
- '!src/index.ts'

profiling:
critical_files_paths: []
critical_files_paths:
- src/location.ts
2 changes: 1 addition & 1 deletion .github/infrastructure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ repository:
automated_security_fixes: true
default_branch: main
delete_branch_on_merge: true
description: utility to convert between positional and offset based locations
description: utility to convert between point and offset based locations
has_issues: true
has_projects: true
has_wiki: false
Expand Down
91 changes: 85 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

[![github release](https://img.shields.io/github/v/release/flex-development/vfile-location.svg?include_prereleases&sort=semver)](https://github.com/flex-development/vfile-location/releases/latest)
[![npm](https://img.shields.io/npm/v/@flex-development/vfile-location.svg)](https://npmjs.com/package/@flex-development/vfile-location)
[![codecov](https://codecov.io/gh/flex-development/vfile-location/graph/badge.svg?token=)](https://codecov.io/gh/flex-development/vfile-location)
[![codecov](https://codecov.io/gh/flex-development/vfile-location/graph/badge.svg?token=81iuGRII5a)](https://codecov.io/gh/flex-development/vfile-location)
[![module type: esm](https://img.shields.io/badge/module%20type-esm-brightgreen)](https://github.com/voxpelli/badges-cjs-esm)
[![license](https://img.shields.io/github/license/flex-development/vfile-location.svg)](LICENSE.md)
[![conventional commits](https://img.shields.io/badge/-conventional%20commits-fe5196?logo=conventional-commits&logoColor=ffffff)](https://conventionalcommits.org/)
[![typescript](https://img.shields.io/badge/-typescript-3178c6?logo=typescript&logoColor=ffffff)](https://typescriptlang.org/)
[![vitest](https://img.shields.io/badge/-vitest-6e9f18?style=flat&logo=vitest&logoColor=ffffff)](https://vitest.dev/)
[![yarn](https://img.shields.io/badge/-yarn-2c8ebb?style=flat&logo=yarn&logoColor=ffffff)](https://yarnpkg.com/)

[vfile][vfile] utility to convert between positional (line and column) and offset (range) based locations
[vfile][vfile] utility to convert between point (line/column) and offset (range) based locations

## Contents

Expand All @@ -19,16 +19,21 @@
- [Install](#install)
- [Use](#use)
- [API](#api)
- [`Location(file[, start])`](#locationfile-start)
- [`Location#offset([point])`](#locationoffsetpoint)
- [`Location#point([offset])`](#locationpointoffset)
- [`Point`](#point)
- [Types](#types)
- [Contribute](#contribute)

## What is this?

**TODO**: what is this?
This is a tiny but useful package that facilitates conversions between [points and offsets][point] in a file.

## When should I use this?

**TODO**: when should i use this?
This utility is useful when adding [*positional information*][positional information] to [unist][unist] nodes, or when
building packages that require location data, such as a set of lint rules.

## Install

Expand Down Expand Up @@ -63,11 +68,79 @@ In browsers with [`esm.sh`][esmsh]:

## Use

**TODO**: use
```ts
import { Location, type Point } from '@flex-development/vfile-location'
import { read } from 'to-vfile'
import type * as unist from 'unist'
import type { VFile, Value } from 'vfile'

const point: Point = { column: 1, line: 21, offset: 474 }
const pt: Point = { column: 2, line: 47, offset: 1124 }

const file: VFile = await read('hrt.ts')
const val: Value = String(file).slice(point.offset, pt.offset + 1)

const location: Location = new Location(file)
const loc: Location = new Location(val, point)

console.log(location.offset({ ...point, offset: undefined })) // => point.offset
console.log(location.point(point.offset)) // => point

console.log(loc.offset({ ...pt, offset: undefined })) // => pt.offset
console.log(loc.point(pt.offset)) // => pt
```

## API

**TODO**: api
This package exports the identifier [`Location`](#locationfile-start). There is no default export.

### `Location(file[, start])`

Create a new location index to translate between point and offset based locations in `file`.

Pass a `start` point to make relative conversions. Any point or offset accessed will be relative to the given point.

- `file` ([`Value`][vfile-value] | [`VFile`][vfile-api]) &mdash; file to index
- `start` ([`Point`](#point) | `null` | `undefined`) &mdash; point before first character in `file`

#### `Location#offset([point])`

Get an offset for `point`.

> 👉 The offset for `point` is greater than or equal to `0` when `point` is valid, and `-1` when `point` is invalid.
##### Parameters

- `point` ([`unist.Point`][point] | `null` | `undefined`) &mdash; place in source file

##### Returns

([`Offset`][offset]) Index of character in source file or `-1`.

#### `Location#point([offset])`

Get a point for `offset`.

> 👉 `point.column` and `point.line` are greater than or equal to `1` when `offset` is valid, and `-1` when `offset` is
> invalid.
##### Parameters

- `offset` ([`Offset`][offset] | `null` | `undefined`) &mdash; index of character in source file

##### Returns

([`Point`](#point)) Place in source file.

### `Point`

One place in a source file (TypeScript interface).

#### Properties

- `column` (`number`) &mdash; column in source file (`1`-indexed integer)
- `line` (`number`) &mdash; line in source file (`1`-indexed integer)
- `offset` ([`Offset`][offset]) &mdash; index of character in source file (`0`-indexed integer)

## Types

Expand All @@ -82,6 +155,12 @@ community you agree to abide by its terms.

[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[esmsh]: https://esm.sh/
[offset]: https://github.com/flex-development/unist-util-types#offset
[point]: https://github.com/syntax-tree/unist#point
[positional information]: https://github.com/syntax-tree/unist#positional-information
[typescript]: https://www.typescriptlang.org
[unist]: https://github.com/syntax-tree/unist
[vfile]: https://github.com/vfile/vfile
[vfile-api]: https://github.com/vfile/vfile#vfileoptions
[vfile-value]: https://github.com/vfile/vfile#value
[yarn]: https://yarnpkg.com
Empty file removed __fixtures__/.gitkeep
Empty file.
49 changes: 49 additions & 0 deletions __fixtures__/hrt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @file Fixtures - hrt
* @module fixtures/hrt
* @see https://codewars.com/kata/52685f7382004e774f0001f7
*/

/**
* Given a non-negative integer, `seconds`, the functions returns the time in a
* human-readable format (`HH:MM:SS`).
*
* @example
* hrt(0) // '00:00:00'
* @example
* hrt(60) // '00:01:00'
* @example
* hrt(359999) // '99:59:59'
*
* @param {number} seconds - Time in seconds
* @return {string} Time in human-readable format (`HH:MM:SS`)
*/
const hrt = (seconds: number): string => {
/**
* {@linkcode seconds} in human-readable format.
*
* @var {string} formatted
*/
let formatted: string = ''

// convert seconds to human-readable time format
for (const converter of [3600, 60, 1]) {
/**
* {@linkcode seconds} in hours, minutes, or seconds.
*
* @const {number} time
*/
const time: number = seconds / converter | 0

// update formatted time
formatted += time < 10 ? `0${time}` : time
if (converter !== 1) formatted += ':'

// remove converted seconds from total seconds
seconds -= time * converter
}

return formatted
}

export default hrt
9 changes: 9 additions & 0 deletions __tests__/setup/faker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @file Test Setup - faker
* @module tests/setup/faker
* @see https://github.com/faker-js/faker
*/

import { faker } from '@faker-js/faker'

global.faker = faker
6 changes: 6 additions & 0 deletions __tests__/setup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @file Entry Point - Test Setup
* @module tests/setup
*/

import './faker'
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flex-development/vfile-location",
"description": "utility to convert between positional (line and column) and offset (range) based locations",
"description": "utility to convert between point (line/column) and offset (range) based locations",
"version": "1.0.0",
"keywords": [
"location",
Expand Down Expand Up @@ -80,6 +80,7 @@
"@arethetypeswrong/cli": "0.15.3",
"@commitlint/cli": "19.3.0",
"@commitlint/types": "19.0.3",
"@faker-js/faker": "9.0.0-alpha.0",
"@flex-development/commitlint-config": "1.0.1",
"@flex-development/decorator-regex": "2.0.0",
"@flex-development/esm-types": "2.0.0",
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/index.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @file E2E Tests - api
* @module vfile-location/tests/e2e/api
*/

import * as testSubject from '../index'

describe('e2e:vfile-location', () => {
it('should expose public api', () => {
expect(testSubject).to.have.keys(['Location'])
})
})
130 changes: 130 additions & 0 deletions src/__tests__/location.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @file Unit Tests - Location
* @module vfile-location/tests/unit/Location
*/

import type { Point } from '#src/interfaces'
import type { Times } from '@flex-development/tutils'
import type { Offset } from '@flex-development/unist-util-types'
import { read } from 'to-vfile'
import type { VFile } from 'vfile'
import TestSubject from '../location'

describe('unit:Location', () => {
let fi: string
let file: VFile
let length: number
let points: Times<6, Point>
let pts: Times<5, Point>
let start: Point

beforeAll(async () => {
file = await read('__fixtures__/hrt.ts')

points = [
{ column: 1, line: 1, offset: 0 },
...(pts = [
start = { column: 1, line: 21, offset: 474 },
{ column: 13, line: 30, offset: 707 },
{ column: 42, line: 40, offset: 1014 },
{ column: 2, line: 47, offset: 1124 },
{ column: 1, line: 50, offset: length = String(file).length }
])
]

fi = String(file).slice(start.offset)
})

describe('#offset', () => {
let subject: TestSubject
let sub: TestSubject

beforeAll(() => {
subject = new TestSubject(file)
sub = new TestSubject(fi, start)
})

it('should return -1 if point.column < 1', () => {
expect(subject.offset({ column: 0, line: 1 })).to.eq(-1)
})

it('should return -1 if point.column is not found', () => {
expect(subject.offset({ column: 40, line: 2 })).to.eq(-1)
})

it('should return -1 if point.line < 1', () => {
expect(subject.offset({ column: 1, line: 0 })).to.eq(-1)
})

it('should return -1 if point.line > total number of lines', () => {
expect(subject.offset({ column: 1, line: 100 })).to.eq(-1)
})

it('should return -1 if point is nil', () => {
;[, null].forEach(offset => expect(subject.offset(offset)).to.eq(-1))
})

it('should return index of character in source file', () => {
points.forEach(point => expect(subject.offset(point)).to.eq(point.offset))
})

it('should return index of character in source file (relative)', () => {
pts.forEach(point => expect(sub.offset(point)).to.eq(point.offset))
})
})

describe('#point', () => {
let subject: TestSubject
let sub: TestSubject

beforeAll(() => {
subject = new TestSubject(file)
sub = new TestSubject(fi, start)
})

it('should return invalid point if offset < 0', () => {
// Arrange
const offset: Offset = faker.number.int({
max: -1,
min: Number.NEGATIVE_INFINITY
})

// Act + Expect
expect(subject.point(offset)).to.eql({ column: -1, line: -1, offset })
})

it('should return invalid point if offset > source file length', () => {
// Arrange
const offset: Offset = faker.number.int({ min: length + 1 })

// Act + Expect
expect(subject.point(offset)).to.eql({ column: -1, line: -1, offset })
})

it('should return invalid point if offset is a float', () => {
// Arrange
const offset: Offset = faker.number.float({ max: length, min: 0 })

// Act + Expect
expect(subject.point(offset)).to.eql({ column: -1, line: -1, offset })
})

it('should return invalid point if offset is nil', () => {
;[, null].forEach(offset => {
expect(subject.point(offset)).to.eql({
column: -1,
line: -1,
offset: -1
})
})
})

it('should return place in source file', () => {
points.forEach(point => expect(subject.point(point.offset)).to.eql(point))
})

it('should return place in source file (relative)', () => {
pts.forEach(point => expect(sub.point(point.offset)).to.eql(point))
})
})
})
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* @module vfile-location
*/

export {}
export type * from './interfaces'
export { default as Location } from './location'
Loading

0 comments on commit 97c894b

Please sign in to comment.