/** * Internal dependencies */ import getPath from './get-path'; export type MatcherFn = (node: Element) => T | undefined; export type MatcherObj = { [key: string]: MatcherObj | MatcherFn }; export type MatcherObjResult = { [K in keyof O]: O[K] extends F ? ReturnType : O[K] extends MatcherObj ? MatcherObjResult : never; }; /** * Function returning a DOM document created by `createHTMLDocument`. The same * document is returned between invocations. * * @return DOM document. */ const getDocument = (() => { let doc: Document | undefined; return () => { if (!doc) { doc = document.implementation.createHTMLDocument(''); } return doc; }; })(); /** * 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 Object of matchers * @return Matched values, shaped by object */ export function parse( source: string | Element, matchers: O ): MatcherObjResult; /** * 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(source: string | Element, matchers: F): ReturnType; /** * 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: O | F ): MatcherObjResult | ReturnType; /** * 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?: 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 (typeof matchers === 'function') { 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, key: keyof MatcherObjResult) => { const inner = matchers[key]; memo[key] = parse(source, inner); return memo; }, {} as MatcherObjResult); } /** * 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 name Property name * @return Property value */ export function prop(name: string): MatcherFn; /** * 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 Property value */ export function prop( selector: string | undefined, name: N ): MatcherFn; /** * 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 Property value */ export function prop( arg1: string | undefined, arg2?: string ): MatcherFn { let name: string; let selector: string | undefined; if (1 === arguments.length) { name = arg1 as string; selector = undefined; } else { name = arg2 as string; selector = arg1; } return function (node: Element): Element[N] | undefined { let match: Element | null = node; if (selector) { match = node.querySelector(selector); } if (match) { return getPath(match, name); } } as MatcherFn; } /** * 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 name Attribute name * @return Attribute value */ export function attr(name: string): MatcherFn; /** * 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 Attribute value */ export function attr(selector: string | undefined, name: string): MatcherFn; /** * 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 Attribute value */ export function attr(arg1: string | undefined, arg2?: string): MatcherFn { let name: string; let selector: string | undefined; if (1 === arguments.length) { name = arg1 as string; selector = undefined; } else { name = arg2 as string; selector = arg1; } return function (node: Element): string | undefined { const attributes = prop(selector, 'attributes')(node); if (attributes && Object.prototype.hasOwnProperty.call(attributes, name)) { return attributes[name as any].value; } }; } /** * Convenience for `prop( selector, 'innerHTML' )`. * * @see prop() * * @param selector Optional selector * @return Inner HTML */ export function html(selector?: string) { return prop(selector, 'innerHTML') as MatcherFn; } /** * Convenience for `prop( selector, 'textContent' )`. * * @see prop() * * @param selector Optional selector * @return Text content */ export function text(selector?: string) { return prop(selector, 'textContent') as MatcherFn; } /** * 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 selector Selector to match * @param matchers Matcher function or object of matchers * @return Matcher function which returns an array of matched value(s) */ export function query( selector: string, matchers?: F | O ): MatcherFn[]> { return function (node: Element) { const matches = node.querySelectorAll(selector); return [].map.call(matches, (match) => parse(match, matchers!)) as MatcherObjResult[]; }; }