Skip to content

Commit 40b99c2

Browse files
authoredDec 1, 2020
refactor: Port module to TS, Jest, ESLint (#7)
BREAKING: The main export is now a `default` export.
2 parents 8d05af6 + a9dd9c4 commit 40b99c2

22 files changed

+13368
-291
lines changed
 

‎.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
coverage/
3+
lib/

‎.eslintrc.json

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"extends": ["eslint:recommended", "prettier"],
3+
"env": {
4+
"node": true,
5+
"es6": true
6+
},
7+
"rules": {
8+
"eqeqeq": [2, "smart"],
9+
"no-caller": 2,
10+
"dot-notation": 2,
11+
"no-var": 2,
12+
"prefer-const": 2,
13+
"prefer-arrow-callback": [2, { "allowNamedFunctions": true }],
14+
"arrow-body-style": [2, "as-needed"],
15+
"object-shorthand": 2,
16+
"prefer-template": 2,
17+
"one-var": [2, "never"],
18+
"prefer-destructuring": [2, { "object": true }],
19+
"capitalized-comments": 2,
20+
"multiline-comment-style": [2, "starred-block"],
21+
"spaced-comment": 2,
22+
"yoda": [2, "never"],
23+
"curly": [2, "multi-line"],
24+
"no-else-return": 2
25+
},
26+
"overrides": [
27+
{
28+
"files": "*.ts",
29+
"extends": [
30+
"plugin:@typescript-eslint/eslint-recommended",
31+
"plugin:@typescript-eslint/recommended",
32+
"prettier/@typescript-eslint"
33+
],
34+
"parserOptions": {
35+
"sourceType": "module",
36+
"project": "./tsconfig.eslint.json"
37+
},
38+
"rules": {
39+
"@typescript-eslint/prefer-for-of": 0,
40+
"@typescript-eslint/member-ordering": 0,
41+
"@typescript-eslint/explicit-function-return-type": 0,
42+
"@typescript-eslint/no-unused-vars": 0,
43+
"@typescript-eslint/no-use-before-define": [
44+
2,
45+
{ "functions": false }
46+
],
47+
"@typescript-eslint/consistent-type-definitions": [
48+
2,
49+
"interface"
50+
],
51+
"@typescript-eslint/prefer-function-type": 2,
52+
"@typescript-eslint/no-unnecessary-type-arguments": 2,
53+
"@typescript-eslint/prefer-string-starts-ends-with": 2,
54+
"@typescript-eslint/prefer-readonly": 2,
55+
"@typescript-eslint/prefer-includes": 2,
56+
"@typescript-eslint/no-unnecessary-condition": 2,
57+
"@typescript-eslint/switch-exhaustiveness-check": 2,
58+
"@typescript-eslint/prefer-nullish-coalescing": 2
59+
}
60+
}
61+
]
62+
}

‎.github/FUNDING.yml

+1-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,2 @@
1-
# These are supported funding model platforms
2-
3-
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4-
patreon: # Replace with a single Patreon username
5-
open_collective: # Replace with a single Open Collective username
6-
ko_fi: # Replace with a single Ko-fi username
1+
github: [fb55]
72
tidelift: "npm/nth-check"
8-
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9-
liberapay: # Replace with a single Liberapay username
10-
issuehunt: # Replace with a single IssueHunt username
11-
otechie: # Replace with a single Otechie username
12-
custom: # Replace with a single custom sponsorship URL

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
.vscode/
12
node_modules/
3+
coverage/
4+
lib/

‎.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
coverage/
3+
lib/

‎.travis.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
language: node_js
22
arch:
3-
- amd64
4-
- ppc64le
3+
- amd64
4+
- ppc64le
55
node_js:
6-
- lts/*
6+
- lts/*

‎README.md

+37-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# nth-check [![Build Status](https://travis-ci.org/fb55/nth-check.svg)](https://travis-ci.org/fb55/nth-check)
22

3-
A performant nth-check parser & compiler.
3+
Parses and compiles CSS nth-checks to highly optimized functions.
44

55
### About
66

@@ -11,43 +11,61 @@ This module can be used to parse & compile nth-checks, as they are found in CSS
1111
### API
1212

1313
```js
14-
const nthCheck = require("nth-check");
14+
import nthCheck, { parse, compile } from "nth-check";
1515
```
1616

1717
##### `nthCheck(formula)`
1818

19-
First parses, then compiles the formula.
19+
Parses and compiles a formula to a highly optimized function. Combination of `parse` and `compile`.
2020

21-
##### `nthCheck.parse(formula)`
21+
If the formula doesn't match any elements, it returns [`boolbase`](https://github.com/fb55/boolbase)'s `falseFunc`. Otherwise, a function accepting an _index_ is returned, which returns whether or not the passed _index_ matches the formula.
2222

23-
Parses the expression, throws a `SyntaxError` if it fails. Otherwise, returns an array containing the integer step size and the integer offset of the nth rule.
23+
**Note**: The nth-rule starts counting at `1`, the returned function at `0`.
2424

25-
__Example:__
25+
**Example:**
2626

2727
```js
28-
nthCheck.parse("2n+3") //[2, 3]
28+
const check = nthCheck("2n+3");
29+
30+
check(0); // `false`
31+
check(1); // `false`
32+
check(2); // `true`
33+
check(3); // `false`
34+
check(4); // `true`
35+
check(5); // `false`
36+
check(6); // `true`
2937
```
3038

31-
##### `nthCheck.compile([a, b])`
39+
##### `parse(formula)`
40+
41+
Parses the expression, throws an `Error` if it fails. Otherwise, returns an array containing the integer step size and the integer offset of the nth rule.
42+
43+
**Example:**
44+
45+
```js
46+
parse("2n+3"); // [2, 3]
47+
```
48+
49+
##### `compile([a, b])`
3250

3351
Takes an array with two elements (as returned by `.parse`) and returns a highly optimized function.
3452

35-
If the formula doesn't match any elements, it returns [`boolbase`](https://github.com/fb55/boolbase)'s `falseFunc`, otherwise, a function accepting an _index_ is returned, which returns whether or not a passed _index_ matches the formula. (Note: The spec starts counting at `1`, the returned function at `0`).
53+
**Example:**
3654

37-
__Example:__
3855
```js
39-
const check = nthCheck.compile([2, 3]);
40-
41-
check(0) //false
42-
check(1) //false
43-
check(2) //true
44-
check(3) //false
45-
check(4) //true
46-
check(5) //false
47-
check(6) //true
56+
const check = compile([2, 3]);
57+
58+
check(0); // `false`
59+
check(1); // `false`
60+
check(2); // `true`
61+
check(3); // `false`
62+
check(4); // `true`
63+
check(5); // `false`
64+
check(6); // `true`
4865
```
4966

5067
---
68+
5169
License: BSD-2-Clause
5270

5371
## Security contact information

‎compile.js

-51
This file was deleted.

‎index.js

-9
This file was deleted.

‎package-lock.json

+12,913-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+65-33
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,67 @@
11
{
2-
"name": "nth-check",
3-
"version": "1.0.2",
4-
"description": "performant nth-check parser & compiler",
5-
"main": "index.js",
6-
"scripts": {
7-
"test": "node test"
8-
},
9-
"repository": {
10-
"type": "git",
11-
"url": "https://github.com/fb55/nth-check"
12-
},
13-
"files": [
14-
"compile.js",
15-
"index.js",
16-
"parse.js"
17-
],
18-
"keywords": [
19-
"nth-child",
20-
"nth",
21-
"css"
22-
],
23-
"author": "Felix Boehm <me@feedic.com>",
24-
"license": "BSD-2-Clause",
25-
"bugs": {
26-
"url": "https://github.com/fb55/nth-check/issues"
27-
},
28-
"homepage": "https://github.com/fb55/nth-check",
29-
"dependencies": {
30-
"boolbase": "~1.0.0"
31-
},
32-
"prettier": {
33-
"tabWidth": 4
34-
}
2+
"name": "nth-check",
3+
"version": "1.0.2",
4+
"description": "Parses and compiles CSS nth-checks to highly optimized functions.",
5+
"author": "Felix Boehm <me@feedic.com>",
6+
"license": "BSD-2-Clause",
7+
"sideEffects": false,
8+
"funding": {
9+
"url": "https://github.com/fb55/nth-check?sponsor=1"
10+
},
11+
"directories": {
12+
"lib": "lib/"
13+
},
14+
"main": "lib/index.js",
15+
"types": "lib/index.d.ts",
16+
"files": [
17+
"lib/**/*"
18+
],
19+
"scripts": {
20+
"test": "jest --coverage && npm run lint",
21+
"coverage": "cat coverage/lcov.info | coveralls",
22+
"lint": "npm run lint:es && npm run lint:prettier",
23+
"lint:es": "eslint .",
24+
"lint:prettier": "npm run prettier -- --check",
25+
"format": "npm run format:es && npm run format:prettier",
26+
"format:es": "npm run lint:es -- --fix",
27+
"format:prettier": "npm run prettier -- --write",
28+
"prettier": "prettier '**/*.{ts,md,json,yml}'",
29+
"build": "tsc",
30+
"prepare": "npm run build"
31+
},
32+
"repository": {
33+
"type": "git",
34+
"url": "https://github.com/fb55/nth-check"
35+
},
36+
"keywords": [
37+
"nth-child",
38+
"nth",
39+
"css"
40+
],
41+
"bugs": {
42+
"url": "https://github.com/fb55/nth-check/issues"
43+
},
44+
"homepage": "https://github.com/fb55/nth-check",
45+
"dependencies": {
46+
"boolbase": "^1.0.0"
47+
},
48+
"devDependencies": {
49+
"@types/jest": "^26.0.0",
50+
"@types/node": "^14.0.5",
51+
"@typescript-eslint/eslint-plugin": "^4.1.0",
52+
"@typescript-eslint/parser": "^4.1.0",
53+
"eslint": "^7.0.0",
54+
"eslint-config-prettier": "^6.0.0",
55+
"jest": "^26.0.1",
56+
"prettier": "^2.1.1",
57+
"ts-jest": "^26.0.0",
58+
"typescript": "^4.0.2"
59+
},
60+
"jest": {
61+
"preset": "ts-jest",
62+
"testEnvironment": "node"
63+
},
64+
"prettier": {
65+
"tabWidth": 4
66+
}
3567
}

‎parse.js

-39
This file was deleted.

‎src/__fixtures__/rules.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export const valid: [string, [number, number]][] = [
2+
["1", [0, 1]],
3+
["2", [0, 2]],
4+
["3", [0, 3]],
5+
["5", [0, 5]],
6+
[" 1 ", [0, 1]],
7+
[" 5 ", [0, 5]],
8+
["+2n + 1", [2, 1]],
9+
["-1", [0, -1]],
10+
["-1n + 3", [-1, 3]],
11+
["-1n+3", [-1, 3]],
12+
["-n+2", [-1, 2]],
13+
["-n+3", [-1, 3]],
14+
["0n+3", [0, 3]],
15+
["1n", [1, 0]],
16+
["1n+0", [1, 0]],
17+
["2n", [2, 0]],
18+
["2n + 1", [2, 1]],
19+
["2n+1", [2, 1]],
20+
["3n", [3, 0]],
21+
["3n+0", [3, 0]],
22+
["3n+1", [3, 1]],
23+
["3n+2", [3, 2]],
24+
["3n+3", [3, 3]],
25+
["3n-1", [3, -1]],
26+
["3n-2", [3, -2]],
27+
["3n-3", [3, -3]],
28+
["even", [2, 0]],
29+
["n", [1, 0]],
30+
["n+2", [1, 2]],
31+
["odd", [2, 1]],
32+
33+
// Surprisingly, neither sizzle, qwery or nwmatcher cover these cases
34+
["-4n+13", [-4, 13]],
35+
["-2n + 12", [-2, 12]],
36+
];
37+
38+
export const invalid = [
39+
"-",
40+
"- 1n",
41+
"-1 n",
42+
"2+0",
43+
"2n+-0",
44+
"an+b",
45+
"asdf",
46+
"b",
47+
"expr",
48+
"odd|even|x",
49+
];

‎src/compile.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import nthCheck, { compile } from ".";
2+
import { valid } from "./__fixtures__/rules";
3+
4+
const valArray = new Array(...Array(2e3)).map((_, i) => i);
5+
6+
/**
7+
* Iterate through all possible values. This is adapted from qwery,
8+
* and uses a more intuitive way to process all elements.
9+
*/
10+
function slowNth([a, b]: [number, number]): number[] {
11+
if (a === 0 && b > 0) return [b - 1];
12+
13+
return valArray.filter((val) => {
14+
for (let i = b; a > 0 ? i <= valArray.length : i >= 1; i += a) {
15+
if (val === valArray[i - 1]) return true;
16+
}
17+
return false;
18+
});
19+
}
20+
21+
describe("parse", () => {
22+
it("compile & run all valid", () => {
23+
for (const [_, parsed] of valid) {
24+
const filtered = valArray.filter(compile(parsed));
25+
const iterated = slowNth(parsed);
26+
27+
expect(filtered).toStrictEqual(iterated);
28+
}
29+
});
30+
31+
it("parse, compile & run all valid", () => {
32+
for (const [rule, parsed] of valid) {
33+
const filtered = valArray.filter(nthCheck(rule));
34+
const iterated = slowNth(parsed);
35+
36+
expect([filtered, rule]).toStrictEqual([iterated, rule]);
37+
}
38+
});
39+
});

‎src/compile.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { trueFunc, falseFunc } from "boolbase";
2+
3+
/**
4+
* Returns a function that checks if an elements index matches the given rule
5+
* highly optimized to return the fastest solution.
6+
*
7+
* @param parsed A tuple [a, b], as returned by `parse`.
8+
* @returns A highly optimized function that returns whether an index matches the nth-check.
9+
* @example
10+
* const check = nthCheck.compile([2, 3]);
11+
*
12+
* check(0); // `false`
13+
* check(1); // `false`
14+
* check(2); // `true`
15+
* check(3); // `false`
16+
* check(4); // `true`
17+
* check(5); // `false`
18+
* check(6); // `true`
19+
*/
20+
export function compile(
21+
parsed: [a: number, b: number]
22+
): (index: number) => boolean {
23+
const a = parsed[0];
24+
// Subtract 1 from `b`, to convert from one- to zero-indexed.
25+
const b = parsed[1] - 1;
26+
27+
/*
28+
* When `b <= 0`, `a * n` won't be lead to any matches for `a < 0`.
29+
* Besides, the specification states that no elements are
30+
* matched when `a` and `b` are 0.
31+
*
32+
* `b < 0` here as we subtracted 1 from `b` above.
33+
*/
34+
if (b < 0 && a <= 0) return falseFunc;
35+
36+
// When `a` is in the range -1..1, it matches any element (so only `b` is checked).
37+
if (a === -1) return (index) => index <= b;
38+
if (a === 0) return (index) => index === b;
39+
// When `b <= 0` and `a === 1`, they match any element.
40+
if (a === 1) return b < 0 ? trueFunc : (index) => index >= b;
41+
42+
/*
43+
* Otherwise, modulo can be used to check if there is a match.
44+
*
45+
* Modulo doesn't care about the sign, so let's use `a`s absolute value.
46+
*/
47+
const absA = Math.abs(a);
48+
// Get `b mod a`, + a if this is negative.
49+
const bMod = ((b % absA) + absA) % absA;
50+
51+
return a > 1
52+
? (index) => index >= b && index % absA === bMod
53+
: (index) => index <= b && index % absA === bMod;
54+
}

‎src/declarations/boolbase.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "boolbase" {
2+
export function trueFunc(...args: unknown[]): true;
3+
export function falseFunc(...args: unknown[]): false;
4+
}

‎src/index.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { parse } from "./parse";
2+
import { compile } from "./compile";
3+
4+
export { parse, compile };
5+
6+
/**
7+
* Parses and compiles a formula to a highly optimized function.
8+
* Combination of `parse` and `compile`.
9+
*
10+
* If the formula doesn't match any elements,
11+
* it returns [`boolbase`](https://github.com/fb55/boolbase)'s `falseFunc`.
12+
* Otherwise, a function accepting an _index_ is returned, which returns
13+
* whether or not the passed _index_ matches the formula.
14+
*
15+
* Note: The nth-rule starts counting at `1`, the returned function at `0`.
16+
*
17+
* @param formula The formula to compile.
18+
* @example
19+
* const check = nthCheck("2n+3");
20+
*
21+
* check(0); // `false`
22+
* check(1); // `false`
23+
* check(2); // `true`
24+
* check(3); // `false`
25+
* check(4); // `true`
26+
* check(5); // `false`
27+
* check(6); // `true`
28+
*/
29+
export default function nthCheck(formula: string): (index: number) => boolean {
30+
return compile(parse(formula));
31+
}

‎src/parse.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { parse } from "./parse";
2+
import { valid, invalid } from "./__fixtures__/rules";
3+
4+
describe("parse", () => {
5+
it("parse invalid", () => {
6+
for (const formula of invalid) {
7+
expect(() => parse(formula)).toThrowError(Error);
8+
}
9+
});
10+
11+
it("parse valid", () => {
12+
for (const [formula, result] of valid) {
13+
expect(parse(formula)).toStrictEqual(result);
14+
}
15+
});
16+
});

‎src/parse.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Following http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
2+
3+
// [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]?
4+
const RE_NTH_ELEMENT = /^([+-]?\d*n)?\s*(?:([+-]?)\s*(\d+))?$/;
5+
6+
/**
7+
* Parses an expression.
8+
*
9+
* @throws An `Error` if parsing fails.
10+
* @returns An array containing the integer step size and the integer offset of the nth rule.
11+
* @example nthCheck.parse("2n+3"); // returns [2, 3]
12+
*/
13+
export function parse(formula: string): [a: number, b: number] {
14+
formula = formula.trim().toLowerCase();
15+
16+
if (formula === "even") {
17+
return [2, 0];
18+
} else if (formula === "odd") {
19+
return [2, 1];
20+
}
21+
22+
const parsed = formula.match(RE_NTH_ELEMENT);
23+
24+
if (!parsed) {
25+
throw new Error(`n-th rule couldn't be parsed ('${formula}')`);
26+
}
27+
28+
let a;
29+
30+
if (parsed[1]) {
31+
a = parseInt(parsed[1], 10);
32+
if (isNaN(a)) {
33+
a = parsed[1].startsWith("-") ? -1 : 1;
34+
}
35+
} else a = 0;
36+
37+
const b =
38+
(parsed[2] === "-" ? -1 : 1) *
39+
(parsed[3] ? parseInt(parsed[3], 10) : 0);
40+
41+
return [a, b];
42+
}

‎test.js

-116
This file was deleted.

‎tsconfig.eslint.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["src", "test"],
4+
"exclude": []
5+
}

‎tsconfig.json

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"compilerOptions": {
3+
/* Basic Options */
4+
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
5+
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6+
// "lib": [], /* Specify library files to be included in the compilation. */
7+
"declaration": true /* Generates corresponding '.d.ts' file. */,
8+
"declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
9+
// "sourceMap": true, /* Generates corresponding '.map' file. */
10+
"outDir": "lib" /* Redirect output structure to the directory. */,
11+
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
12+
13+
/* Strict Type-Checking Options */
14+
"strict": true /* Enable all strict type-checking options. */,
15+
16+
/* Additional Checks */
17+
"noUnusedLocals": true /* Report errors on unused locals. */,
18+
"noUnusedParameters": true /* Report errors on unused parameters. */,
19+
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
20+
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
21+
22+
/* Module Resolution Options */
23+
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
24+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
25+
"resolveJsonModule": true,
26+
27+
"paths": {
28+
"*": ["src/declarations/*", "*"]
29+
}
30+
},
31+
"include": ["src"],
32+
"exclude": [
33+
"**/*.spec.ts",
34+
"**/__fixtures__/*",
35+
"**/__tests__/*",
36+
"**/__snapshots__/*"
37+
]
38+
}

0 commit comments

Comments
 (0)
Please sign in to comment.