Skip to content

Commit

Permalink
Port to TypeScript
Browse files Browse the repository at this point in the history
aduth committed Mar 19, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent f47702d commit 489bcdf
Showing 15 changed files with 1,503 additions and 572 deletions.
12 changes: 8 additions & 4 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"presets": [
[ "@babel/env", {
"modules": false
} ]
"@babel/preset-typescript",
[
"@babel/env",
{
"modules": false
}
]
],
"env": {
"test": {
"presets": [ "@babel/env" ]
"presets": ["@babel/preset-typescript", "@babel/env"]
}
}
}
12 changes: 3 additions & 9 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
{
"root": true,
"extends": ["eslint:recommended", "prettier"],
"plugins": ["prettier"],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": "@aduth/eslint-config",
"env": {
"es6": true,
"browser": true,
"mocha": true,
"node": true
},
"rules": {
"prettier/prettier": "error"
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": "error"
}
}
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/es/*
!/es/index.d.ts
es/
node_modules/
*.log
dist/
127 changes: 0 additions & 127 deletions es/index.d.ts

This file was deleted.

3 changes: 3 additions & 0 deletions mocha-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import register from '@babel/register';

register({ extensions: ['.ts'] });
1,361 changes: 1,228 additions & 133 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -19,14 +19,15 @@
"src"
],
"scripts": {
"build:es": "babel src --out-dir es",
"build:es": "babel src --extensions '.ts' --out-dir es",
"build:umd": "rollup -c",
"build:types": "tsc -b",
"build": "npm run build:es && npm run build:umd",
"build:types": "tsc -b tsconfig.decl.json",
"build": "npm run build:es && npm run build:umd && npm run build:types",
"dev": "rollup -c -w",
"lint": "eslint . --ignore-pattern dist --ignore-pattern es",
"unit-test": "NODE_ENV=test mocha --require @babel/register",
"test": "npm run unit-test && npm run lint",
"unit-test": "NODE_ENV=test mocha -r jsdom-global/register -r @babel/register -r ./mocha-setup.js --extension ts",
"typecheck": "tsc",
"test": "npm run unit-test && npm run lint && npm run typecheck",
"prepublishOnly": "npm run build"
},
"author": {
@@ -36,17 +37,25 @@
},
"license": "MIT",
"devDependencies": {
"@aduth/eslint-config": "^4.4.1",
"@babel/cli": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.21.0",
"@babel/register": "^7.18.9",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.4.0",
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"chai": "^4.3.7",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-prettier": "^4.2.1",
"jsdom": "^21.1.0",
"jsdom-global": "^3.0.2",
"mocha": "^10.2.0",
"prettier": "^2.8.4",
"rollup": "^3.15.0",
9 changes: 7 additions & 2 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import resolve from '@rollup/plugin-node-resolve';

export default [
{
input: 'src/index.js',
input: 'src/index.ts',
output: {
format: 'umd',
name: 'hpq',
file: 'dist/hpq.js',
},
plugins: [
resolve({ extensions: ['.ts'] }),
babel({
extensions: ['.ts'],
babelHelpers: 'bundled',
exclude: 'node_modules/**',
}),
],
},
{
input: 'src/index.js',
input: 'src/index.ts',
output: {
format: 'umd',
name: 'hpq',
file: 'dist/hpq.min.js',
},
plugins: [
resolve({ extensions: ['.ts'] }),
babel({
extensions: ['.ts'],
babelHelpers: 'bundled',
exclude: 'node_modules/**',
}),
8 changes: 4 additions & 4 deletions src/get-path.js → src/get-path.ts
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@
* Given object and string of dot-delimited path segments, returns value at
* path or undefined if path cannot be resolved.
*
* @param {Record<string, any>} object Lookup object
* @param {string} path Path to resolve
* @return {*=} Resolved value
* @param object Lookup object
* @param path Path to resolve
* @return Resolved value
*/
export default function getPath(object, path) {
export default function getPath(object: Record<string, any>, path: string): any {
const segments = path.split('.');

let segment;
255 changes: 0 additions & 255 deletions src/index.js

This file was deleted.

212 changes: 212 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/**
* Internal dependencies
*/
import getPath from './get-path';

/**
* Function returning a DOM document created by `createHTMLDocument`. The same
* document is returned between invocations.
*
* @return DOM document.
*/
const getDocument = (() => {
let doc: Document;
return (): Document => {
if (!doc) {
doc = document.implementation.createHTMLDocument('');
}

return doc;
};
})();

type MatcherFn<R = any> = (node: Element) => R;

type MatcherObj = { [x: string]: MatcherObj | MatcherFn };

type MatcherObjResult<F extends MatcherFn, O extends MatcherObj> = {
[K in keyof O]: O[K] extends F
? ReturnType<O[K]>
: O[K] extends MatcherObj
? MatcherObjResult<F, O[K]>
: never;
};

/**
* Given a markup string or DOM element, creates an object aligning with the
* shape of the matchers object, or the value returned by the matcher.
*
* @param source Source content
* @param matchers Object of matchers
* @return Matched values, shaped by object
*/
export function parse<F extends MatcherFn, O extends MatcherObj>(
source: string | Element,
matchers: O
): MatcherObjResult<F, O>;

/**
* Given a markup string or DOM element, creates an object aligning with the
* shape of the matchers object, or the value returned by the matcher.
*
* @param source Source content
* @param matcher Matcher function
* @return Matched value
*/
export function parse<F extends MatcherFn>(source: string | Element, matchers: F): ReturnType<F>;

/**
* Given a markup string or DOM element, creates an object aligning with the
* shape of the matchers object, or the value returned by the matcher.
*
* @param source Source content
* @param matchers Matcher function or object of matchers
*/
export function parse<F extends MatcherFn, O extends MatcherObj>(
source: string | Element,
matchers: O | F
): MatcherObjResult<F, O> | ReturnType<F>;

/**
* Given a markup string or DOM element, creates an object aligning with the
* shape of the matchers object, or the value returned by the matcher.
*
* @param source Source content
* @param matchers Matcher function or object of matchers
*/
export function parse(source: string | Element, matchers?: undefined): undefined;

/**
* Given a markup string or DOM element, creates an object aligning with the
* shape of the matchers object, or the value returned by the matcher.
*
* @param source Source content
* @param matchers Matcher function or object of matchers
*/
export function parse<F extends MatcherFn, O extends MatcherObj>(
source: string | Element,
matchers?: O | F
) {
if (!matchers) {
return;
}

// Coerce to element
if ('string' === typeof source) {
const doc = getDocument();
doc.body.innerHTML = source;
source = doc.body;
}

// Return singular value
if ('function' === typeof matchers) {
return matchers(source);
}

// Bail if we can't handle matchers
if (Object !== matchers.constructor) {
return;
}

// Shape result by matcher object
return Object.keys(matchers).reduce((memo: MatcherObj, key) => {
const inner = matchers[key];
memo[key] = parse(source, inner);
return memo;
}, {});
}

/**
* Generates a function which matches node of type selector, returning an
* attribute by property if the attribute exists. If no selector is passed,
* returns property of the query element.
*
* @param selector Optional selector
* @param name Property name
* @return Matcher function returning the property value
*/
export function prop<N extends keyof Element>(selector?: string, name?: N): MatcherFn<Element[N]> {
if (1 === arguments.length) {
name = selector as N;
selector = undefined;
}

return function (node) {
let match: Element | null = node;
if (selector) {
match = node.querySelector(selector);
}

if (match) {
return getPath(match, name!);
}
};
}

/**
* Generates a function which matches node of type selector, returning an
* attribute by name if the attribute exists. If no selector is passed,
* returns attribute of the query element.
*
* @param selector Optional selector
* @param name Attribute name
* @return Matcher function returning the attribute value
*/
export function attr(selector?: string, name?: string): MatcherFn<string | undefined> {
if (1 === arguments.length) {
name = selector;
selector = undefined;
}

return function (node) {
const attributes = prop(selector, 'attributes')(node);
if (attributes && Object.prototype.hasOwnProperty.call(attributes, name!)) {
return attributes.getNamedItem(name!)!.value;
}
};
}

/**
* Convenience for `prop( selector, 'innerHTML' )`.
*
* @see prop()
*
* @param selector Optional selector
* @return Matcher which returns innerHTML
*/
export function html(selector?: string): MatcherFn<string> {
return prop(selector, 'innerHTML') as MatcherFn<string>;
}

/**
* Convenience for `prop( selector, 'textContent' )`.
*
* @see prop()
*
* @param selector Optional selector
* @return Matcher which returns text content
*/
export function text(selector?: string): MatcherFn<string> {
return prop(selector, 'textContent') as MatcherFn<string>;
}

/**
* Creates a new matching context by first finding elements matching selector
* using querySelectorAll before then running another `parse` on `matchers`
* scoped to the matched elements.
*
* @see parse()
*
* @param {string} selector Selector to match
* @param {Matcher<T>} matchers Matcher function or object of matchers
* @return Matcher function which returns an array of matched value(s)
*/
export function query<F extends MatcherFn, O extends MatcherObj>(
selector: string,
matchers?: F | O
): MatcherFn<MatcherObjResult<F, O>[]> {
return function (node) {
const matches = node.querySelectorAll(selector);
return [].map.call(matches, (match) => parse(match, matchers!)) as MatcherObjResult<F, O>[];
};
}
File renamed without changes.
22 changes: 3 additions & 19 deletions test/index.js → test/index.ts
Original file line number Diff line number Diff line change
@@ -14,23 +14,12 @@ describe('hpq', () => {
'<blockquote><p>…</p><p>…</p><cite class="large"><em>—</em> Andrew</cite></blockquote>';

// Element
let element;
let element: HTMLElement;

before(() => {
const { JSDOM } = require('jsdom');
global.window = new JSDOM('').window;
global.document = window.document;
global.navigator = window.navigator;

element = document.createElement('div');
element.innerHTML = markup;
element = element.firstChild;
});

after(() => {
delete global.document;
delete global.window;
delete global.navigator;
element = element.firstChild as HTMLElement;
});

describe('parse()', () => {
@@ -53,6 +42,7 @@ describe('hpq', () => {
});

it('should return undefiend if passed matchers other than object, function', () => {
// @ts-ignore
const result = parse(element, 2);

expect(result).to.be.undefined;
@@ -180,12 +170,6 @@ describe('hpq', () => {
});

describe('query()', () => {
it('should return a matcher function', () => {
const matcher = query();

expect(matcher).to.be.a('function');
});

it('should return array of parse on matched nodes', () => {
const result = parse(element, { text: query('p', text()) });

11 changes: 11 additions & 0 deletions tsconfig.decl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./es",
"rootDir": "src",
"noEmit": false
},
"include": ["src"]
}
19 changes: 8 additions & 11 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["ES2017", "DOM", "DOM.Iterable"],
"outDir": "./es",
"strict": true,
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true
},
"include": ["src/**/*.js"]
"compilerOptions": {
"target": "ES2017",
"lib": ["ES2017", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"strict": true,
"noEmit": true
},
"include": ["src", "test"]
}

0 comments on commit 489bcdf

Please sign in to comment.