Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance types #18

Merged
merged 21 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"main": "dist/hpq.js",
"module": "es/index.js",
"types": "es/index.d.ts",
"files": [
"dist",
"es",
Expand All @@ -20,7 +21,8 @@
"scripts": {
"build:es": "babel src --out-dir es",
"build:umd": "rollup -c",
"build": "npm run build:es && npm run build:umd",
"build:types": "tsc -b",
"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",
Expand All @@ -47,6 +49,7 @@
"jsdom": "^21.1.0",
"mocha": "^10.2.0",
"prettier": "^2.8.4",
"rollup": "^3.15.0"
"rollup": "^3.15.0",
"typescript": "^5.0.2"
}
}
6 changes: 3 additions & 3 deletions src/get-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Given object and string of dot-delimited path segments, returns value at
* path or undefined if path cannot be resolved.
*
* @param {Object} object Lookup object
* @param {string} path Path to resolve
* @return {?*} Resolved value
* @param {Record<string, any>} object Lookup object
* @param {string} path Path to resolve
* @return {*=} Resolved value
*/
export default function getPath(object, path) {
const segments = path.split('.');
Expand Down
130 changes: 104 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import getPath from './get-path';
* @return {Document} DOM document.
*/
const getDocument = (() => {
/** @type {Document|null} */
let doc;
return () => {
if (!doc) {
Expand All @@ -20,13 +21,50 @@ const getDocument = (() => {
};
})();

/**
* @typedef {(node: Element) => T} MatcherFn
* @template T
*/

/**
* @typedef {Record<string, MatcherFn<T>>} MatcherObj
* @template T
*/

/**
* @typedef {(MatcherFn<T>|MatcherObj<T>)} Matcher
* @template T
*/

/**
* @template T
* @template {MatcherObj<T>} O
*
* @overload
* @param {Element|string} source
* @param {O} matchers
* @return {{ [K in keyof O]: ReturnType<O[K]> }}

*/

/**
* @template {any} T
johnhooks marked this conversation as resolved.
Show resolved Hide resolved
* @template {MatcherFn<T>} F
*
* @overload
* @param {Element|string} source
* @param {F} matchers
* @return {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 {(string|Element)} source Source content
* @param {(Object|Function)} matchers Matcher function or object of matchers
* @return {(Object|*)} Matched value(s), shaped by object
* @param {string|Element} source Source content
* @param {Matcher<string>} matchers Matcher function or object of matchers
johnhooks marked this conversation as resolved.
Show resolved Hide resolved
* @return {any} Matched value(s), shaped by object
*/
export function parse(source, matchers) {
if (!matchers) {
Expand All @@ -42,7 +80,7 @@ export function parse(source, matchers) {

// Return singular value
if ('function' === typeof matchers) {
return matchers(source);
return /** @type {ReturnType<F>} */ (matchers(/** @type {Element} */ (source)));
}

// Bail if we can't handle matchers
Expand All @@ -51,20 +89,40 @@ export function parse(source, matchers) {
}

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

return /** @type {{ [K in keyof O]: ReturnType<O[K]>; }} */ (
Object.keys(matchers).reduce((memo, key) => {
memo[key] = parse(source, matchers[key]);
return memo;
}, /** @type {Record<string, any>} */ ({}))
);
}

/**
* @template T
*
* @overload
* @param {string} property name
* @return {MatcherFn<T>} Matcher function returning the property value
*/

/**
* @template T
*
* @overload
* @param {string|undefined} selector Optional selector
* @param {string} name Property name
* @return {MatcherFn<T>} Matcher function returning the property value
*/

/**
* 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 {?string} selector Optional selector
* @param {string} name Property name
* @return {*} Property value
* @param {string=} selector Optional selector
* @param {string=} name Property name
* @return {MatcherFn<T>} Matcher function returning the property value
*/
export function prop(selector, name) {
if (1 === arguments.length) {
Expand All @@ -73,25 +131,39 @@ export function prop(selector, name) {
}

return function (node) {
/** @type {Element|null} */
let match = node;
if (selector) {
match = node.querySelector(selector);
}

if (match) {
return getPath(match, name);
return getPath(match, /** @type {string} */ (name));
}
};
}

/**
* @overload
* @param {string} name Attribute name
* @return {MatcherFn<string>} Matcher function returning the attribute value
*/

/**
* @overload
* @param {string|undefined} selector Optional selector
* @param {string} name Attribute name
* @return {MatcherFn<string>} Matcher function returning the attribute value
*/

/**
* 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 {?string} selector Optional selector
* @param {string} name Attribute name
* @return {?string} Attribute value
* @param {string=} selector Optional selector
* @param {string=} name Attribute name
* @return {MatcherFn<string>} Matcher function returning the attribute value
*/
export function attr(selector, name) {
if (1 === arguments.length) {
Expand All @@ -100,9 +172,12 @@ export function attr(selector, name) {
}

return function (node) {
const attributes = prop(selector, 'attributes')(node);
if (attributes && Object.prototype.hasOwnProperty.call(attributes, name)) {
return attributes[name].value;
const attributes = /** @type {NamedNodeMap}} */ (prop(selector, 'attributes')(node));
if (
attributes &&
Object.prototype.hasOwnProperty.call(attributes, /** @type {string} */ (name))
) {
return attributes[/** @type {string} */ (name)].value;
}
};
}
Expand All @@ -112,8 +187,8 @@ export function attr(selector, name) {
*
* @see prop()
*
* @param {?string} selector Optional selector
* @return {string} Inner HTML
* @param {string=} selector Optional selector
* @return {(node: Element) => string} Matcher which returns innerHTML
*/
export function html(selector) {
return prop(selector, 'innerHTML');
Expand All @@ -124,8 +199,8 @@ export function html(selector) {
*
* @see prop()
*
* @param {?string} selector Optional selector
* @return {string} Text content
* @param {string} selector Optional selector
* @return {(node: Element) => string} Matcher which returns text content
*/
export function text(selector) {
return prop(selector, 'textContent');
Expand All @@ -138,13 +213,16 @@ export function text(selector) {
*
* @see parse()
*
* @param {string} selector Selector to match
* @param {(Object|Function)} matchers Matcher function or object of matchers
* @return {Array.<*,Object>} Array of matched value(s)
* @template {unknown} T
*
* @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(selector, matchers) {
/** @type {(node: Element) => any[]} */
return function (node) {
const matches = node.querySelectorAll(selector);
return [].map.call(matches, (match) => parse(match, matchers));
return [].map.call(matches, (/** @type {Element} */ match) => parse(match, matchers));
};
}
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["ES2017", "DOM", "DOM.Iterable"],
"outDir": "./es",
"strict": true,
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true
},
"include": ["src/**/*.js"]
}