diff --git a/.github/workflows/bb.yml b/.github/workflows/bb.yml new file mode 100644 index 0000000..291ab09 --- /dev/null +++ b/.github/workflows/bb.yml @@ -0,0 +1,13 @@ +name: bb +on: + issues: + types: [opened, reopened, edited, closed, labeled, unlabeled] + pull_request: + types: [opened, reopened, edited, closed, labeled, unlabeled] +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: unifiedjs/beep-boop-beta@main + with: + repo-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ffb6759..fe284ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/dubnium + - lts/erbium - node diff --git a/.gitignore b/.gitignore index fdefc8c..c977c85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store +*.d.ts *.log -.nyc_output/ coverage/ node_modules/ yarn.lock diff --git a/.prettierignore b/.prettierignore index e7939c4..cebe81f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ coverage/ -*.json *.md diff --git a/index.js b/index.js index 756e261..5709838 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,33 @@ -'use strict' +/** + * @typedef {import('unist').Node} Node + */ -exports.matches = matches -exports.selectAll = selectAll -exports.select = select +import {any} from './lib/any.js' +import {parse} from './lib/parse.js' -var any = require('./lib/any') -var parse = require('./lib/parse') - -function matches(selector, node) { - return Boolean( - any(parse(selector), node, {one: true, shallow: true, any: any})[0] - ) +/** + * @param {string} selector + * @param {Node} [node] + * @returns {boolean} + */ +export function matches(selector, node) { + return Boolean(any(parse(selector), node, {one: true, shallow: true, any})[0]) } -function select(selector, node) { - return any(parse(selector), node, {one: true, any: any})[0] || null +/** + * @param {string} selector + * @param {Node} [node] + * @returns {Node|null} + */ +export function select(selector, node) { + return any(parse(selector), node, {one: true, any})[0] || null } -function selectAll(selector, node) { - return any(parse(selector), node, {any: any}) +/** + * @param {string} selector + * @param {Node} [node] + * @returns {Array.} + */ +export function selectAll(selector, node) { + return any(parse(selector), node, {any}) } diff --git a/lib/any.js b/lib/any.js index 6ed4524..a1f9df0 100644 --- a/lib/any.js +++ b/lib/any.js @@ -1,26 +1,43 @@ -'use strict' - -module.exports = match - -var zwitch = require('zwitch') -var pseudo = require('./pseudo') -var test = require('./test') -var nest = require('./nest') +/** + * @typedef {import('./types.js').Selector} Selector + * @typedef {import('./types.js').Selectors} Selectors + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').RuleSet} RuleSet + * @typedef {import('./types.js').RulePseudo} RulePseudo + * @typedef {import('./types.js').Query} Query + * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').SelectIterator} SelectIterator + * @typedef {import('./types.js').SelectState} SelectState + */ + +import {zwitch} from 'zwitch' +import {nest} from './nest.js' +import {pseudo} from './pseudo.js' +import {test} from './test.js' +import {root} from './util.js' var type = zwitch('type', { unknown: unknownType, invalid: invalidType, - handlers: { - selectors: selectors, - ruleSet: ruleSet, - rule: rule - } + handlers: {selectors, ruleSet, rule} }) -function match(query, node, state) { +/** + * @param {Selectors|RuleSet|Rule} query + * @param {Node} node + * @param {SelectState} state + */ +export function any(query, node, state) { + // @ts-ignore zwitch types are off. return query && node ? type(query, node, state) : [] } +/** + * @param {Selectors} query + * @param {Node} node + * @param {SelectState} state + */ function selectors(query, node, state) { var collect = collector(state.one) var index = -1 @@ -32,10 +49,20 @@ function selectors(query, node, state) { return collect.result } +/** + * @param {RuleSet} query + * @param {Node} node + * @param {SelectState} state + */ function ruleSet(query, node, state) { return rule(query.rule, node, state) } +/** + * @param {Rule} query + * @param {Node} tree + * @param {SelectState} state + */ function rule(query, tree, state) { var collect = collector(state.one) @@ -49,8 +76,9 @@ function rule(query, tree, state) { 0, null, configure(query, { - scopeNodes: tree.type === 'root' ? tree.children : [tree], - iterator: iterator, + scopeNodes: root(tree) ? tree.children : [tree], + index: false, + iterator, one: state.one, shallow: state.shallow, any: state.any @@ -59,9 +87,10 @@ function rule(query, tree, state) { return collect.result + /** @type {SelectIterator} */ function iterator(query, node, index, parent, state) { if (test(query, node, index, parent, state)) { - if (query.rule) { + if ('rule' in query) { nest(query.rule, node, index, parent, configure(query.rule, state)) } else { collect(node) @@ -71,12 +100,18 @@ function rule(query, tree, state) { } } +/** + * @template {SelectState} S + * @param {Rule} query + * @param {S} state + * @returns {S} + */ function configure(query, state) { var pseudos = query.pseudos || [] var index = -1 while (++index < pseudos.length) { - if (pseudo.needsIndex.indexOf(pseudos[index].name) > -1) { + if (pseudo.needsIndex.includes(pseudos[index].name)) { state.index = true break } @@ -85,25 +120,39 @@ function configure(query, state) { return state } -/* istanbul ignore next - Shouldn’t be invoked, all data is handled. */ +// Shouldn’t be invoked, all data is handled. +/* c8 ignore next 6 */ +/** + * @param {{[x: string]: unknown, type: string}} query + */ function unknownType(query) { throw new Error('Unknown type `' + query.type + '`') } -/* istanbul ignore next - Shouldn’t be invoked, parser gives correct data. */ +// Shouldn’t be invoked, parser gives correct data. +/* c8 ignore next 3 */ function invalidType() { throw new Error('Invalid type') } +/** + * @param {boolean} one + */ function collector(one) { + /** @type {Array.} */ var result = [] + /** @type {boolean} */ var found collect.result = result return collect - /* Append nodes to array, filtering out duplicates. */ + /** + * Append nodes to array, filtering out duplicates. + * + * @param {Node|Array.} node + */ function collect(node) { var index = -1 @@ -116,14 +165,18 @@ function collector(one) { } } + /** + * @param {Node} node + */ function collectOne(node) { if (one) { - /* istanbul ignore if - shouldn’t happen, safeguards performance problems. */ + /* Shouldn’t happen, safeguards performance problems. */ + /* c8 ignore next */ if (found) throw new Error('Cannot collect multiple nodes') found = true } - if (result.indexOf(node) < 0) result.push(node) + if (!result.includes(node)) result.push(node) } } diff --git a/lib/attribute.js b/lib/attribute.js index c1e1420..124c56b 100644 --- a/lib/attribute.js +++ b/lib/attribute.js @@ -1,8 +1,10 @@ -'use strict' +/** + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').RuleAttr} RuleAttr + * @typedef {import('./types.js').Node} Node + */ -module.exports = match - -var zwitch = require('zwitch') +import {zwitch} from 'zwitch' var handle = zwitch('operator', { unknown: unknownOperator, @@ -16,39 +18,56 @@ var handle = zwitch('operator', { } }) -function match(query, node) { - var attrs = query.attrs +/** + * @param {Rule} query + * @param {Node} node + */ +export function attribute(query, node) { var index = -1 - while (++index < attrs.length) { - if (!handle(attrs[index], node)) return false + while (++index < query.attrs.length) { + if (!handle(query.attrs[index], node)) return false } return true } -// [attr] +/** + * `[attr]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function exists(query, node) { - return node[query.name] != null + return node[query.name] !== null && node[query.name] !== undefined } -// [attr=value] +/** + * `[attr=value]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function exact(query, node) { - return node[query.name] != null && String(node[query.name]) === query.value + return exists(query, node) && String(node[query.name]) === query.value } -// [attr~=value] +/** + * `[attr~=value]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function containsArray(query, node) { var value = node[query.name] - if (value == null) return false + if (value === null || value === undefined) return false // If this is an array, and the query is contained in it, return true. - if ( - typeof value === 'object' && - 'length' in value && - value.indexOf(query.value) > -1 - ) { + // Coverage comment in place because TS turns `Array.isArray(unknown)` + // into `Array.` instead of `Array.`. + // type-coverage:ignore-next-line + if (Array.isArray(value) && value.includes(query.value)) { return true } @@ -56,7 +75,12 @@ function containsArray(query, node) { return String(value) === query.value } -// [attr^=value] +/** + * `[attr^=value]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function begins(query, node) { var value = node[query.name] @@ -66,7 +90,12 @@ function begins(query, node) { ) } -// [attr$=value] +/** + * `[attr$=value]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function ends(query, node) { var value = node[query.name] @@ -76,13 +105,22 @@ function ends(query, node) { ) } -// [attr*=value] +/** + * `[attr*=value]` + * + * @param {RuleAttr} query + * @param {Node} node + */ function containsString(query, node) { var value = node[query.name] - return typeof value === 'string' && value.indexOf(query.value) > -1 + return typeof value === 'string' && value.includes(query.value) } -/* istanbul ignore next - Shouldn’t be invoked, Parser throws an error instead. */ +// Shouldn’t be invoked, Parser throws an error instead. +/* c8 ignore next 6 */ +/** + * @param {{[x: string]: unknown, type: string}} query + */ function unknownOperator(query) { throw new Error('Unknown operator `' + query.operator + '`') } diff --git a/lib/name.js b/lib/name.js index 3066020..d5dc2c1 100644 --- a/lib/name.js +++ b/lib/name.js @@ -1,7 +1,12 @@ -'use strict' +/** + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').Node} Node + */ -module.exports = match - -function match(query, node) { +/** + * @param {Rule} query + * @param {Node} node + */ +export function name(query, node) { return query.tagName === '*' || query.tagName === node.type } diff --git a/lib/nest.js b/lib/nest.js index c03c372..87221e6 100644 --- a/lib/nest.js +++ b/lib/nest.js @@ -1,8 +1,15 @@ -'use strict' - -module.exports = match - -var zwitch = require('zwitch') +/** + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').Query} Query + * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').SelectState} SelectState + * @typedef {import('./types.js').SelectIterator} SelectIterator + * @typedef {import('./types.js').Handler} Handler + */ + +import {zwitch} from 'zwitch' +import {parent} from './util.js' var own = {}.hasOwnProperty @@ -17,98 +24,116 @@ var handle = zwitch('nestingOperator', { } }) -function match(query, node, index, parent, state) { +/** @type {Handler} */ +export function nest(query, node, index, parent, state) { return handle(query, node, index, parent, state) } -/* istanbul ignore next - Shouldn’t be invoked, parser gives correct data. */ +// Shouldn’t be invoked, parser gives correct data. +/* c8 ignore next 6 */ +/** + * @param {{[x: string]: unknown, type: string}} query + */ function unknownNesting(query) { throw new Error('Unexpected nesting `' + query.nestingOperator + '`') } +/** @type {Handler} */ function topScan(query, node, index, parent, state) { - /* istanbul ignore if - Shouldn’t happen. */ + // Shouldn’t happen. + /* c8 ignore next 3 */ if (parent) { throw new Error('topScan is supposed to be called from the root node') } - state.iterator.apply(null, arguments) - - if (!state.shallow) descendant.apply(this, arguments) + state.iterator(query, node, index, parent, state) + if (!state.shallow) descendant(query, node, index, parent, state) } +/** @type {Handler} */ function descendant(query, node, index, parent, state) { var previous = state.iterator state.iterator = iterator + child(query, node, index, parent, state) - child.apply(this, arguments) - - function iterator(_, node, index, parent, state) { + /** @type {SelectIterator} */ + function iterator(query, node, index, parent, state) { state.iterator = previous - previous.apply(this, arguments) + previous(query, node, index, parent, state) state.iterator = iterator if (state.one && state.found) return - child.call(this, query, node, index, parent, state) + child(query, node, index, parent, state) } } -function child(query, node, index, parent, state) { - if (!node.children || !node.children.length) return +/** @type {Handler} */ +function child(query, node, _1, _2, state) { + if (!parent(node)) return + if (node.children.length === 0) return - walkIterator(query, node, state).each().done() + new WalkIterator(query, node, state).each().done() } -function adjacentSibling(query, node, index, parent, state) { - /* istanbul ignore if - Shouldn’t happen. */ +/** @type {Handler} */ +function adjacentSibling(query, _, index, parent, state) { + // Shouldn’t happen. + /* c8 ignore next */ if (!parent) return - walkIterator(query, parent, state) + new WalkIterator(query, parent, state) .prefillTypeIndex(0, ++index) .each(index, ++index) .prefillTypeIndex(index) .done() } -function generalSibling(query, node, index, parent, state) { - /* istanbul ignore if - Shouldn’t happen. */ +/** @type {Handler} */ +function generalSibling(query, _, index, parent, state) { + // Shouldn’t happen. + /* c8 ignore next */ if (!parent) return - walkIterator(query, parent, state) + new WalkIterator(query, parent, state) .prefillTypeIndex(0, ++index) .each(index) .done() } -// Handles typeIndex and typeCount properties for every walker. -function walkIterator(query, parent, state) { - var typeIndex = state.index && createTypeIndex() - var siblings = parent.children - var delayed = [] - - return { - prefillTypeIndex: rangeDefaults(prefillTypeIndex), - each: rangeDefaults(each), - done: done +class WalkIterator { + /** + * Handles typeIndex and typeCount properties for every walker. + * + * @param {Rule} query + * @param {Parent} parent + * @param {SelectState} state + */ + constructor(query, parent, state) { + /** @type {Rule} */ + this.query = query + /** @type {Parent} */ + this.parent = parent + /** @type {SelectState} */ + this.state = state + /** @type {TypeIndex|undefined} */ + this.typeIndex = state.index ? new TypeIndex() : undefined + /** @type {Array.} */ + this.delayed = [] } - function done() { - var index = -1 - - while (++index < delayed.length) { - delayed[index]() - if (state.one && state.found) break - } + /** + * @param {number|null|undefined} [x] + * @param {number|null|undefined} [y] + * @returns {this} + */ + prefillTypeIndex(x, y) { + var [start, end] = this.defaults(x, y) - return this - } - - function prefillTypeIndex(start, end) { - if (typeIndex) { + if (this.typeIndex) { while (start < end) { - typeIndex(siblings[start]) + this.typeIndex.index(this.parent.children[start]) start++ } } @@ -116,66 +141,102 @@ function walkIterator(query, parent, state) { return this } - function each(start, end) { - var child = siblings[start] + /** + * @param {number|null|undefined} [x] + * @param {number|null|undefined} [y] + * @returns {this} + */ + each(x, y) { + var [start, end] = this.defaults(x, y) + var child = this.parent.children[start] + /** @type {number} */ var index + /** @type {number} */ var nodeIndex if (start >= end) return this - if (typeIndex) { - nodeIndex = typeIndex.nodes - index = typeIndex(child) - delayed.push(delay) + if (this.typeIndex) { + nodeIndex = this.typeIndex.nodes + index = this.typeIndex.index(child) + this.delayed.push(delay) } else { - state.iterator(query, child, start, parent, state) + this.state.iterator(this.query, child, start, this.parent, this.state) } // Stop if we’re looking for one node and it’s already found. - if (state.one && state.found) return this + if (this.state.one && this.state.found) return this - return each.call(this, start + 1, end) + return this.each(start + 1, end) + /** + * @this {WalkIterator} + */ function delay() { - state.typeIndex = index - state.nodeIndex = nodeIndex - state.typeCount = typeIndex.count(child) - state.nodeCount = typeIndex.nodes - state.iterator(query, child, start, parent, state) + this.state.typeIndex = index + this.state.nodeIndex = nodeIndex + this.state.typeCount = this.typeIndex.count(child) + this.state.nodeCount = this.typeIndex.nodes + this.state.iterator(this.query, child, start, this.parent, this.state) } } - function rangeDefaults(iterator) { - return rangeDefault + /** + * Done! + * @returns {this} + */ + done() { + var index = -1 - function rangeDefault(start, end) { - if (start == null || start < 0) start = 0 - if (end == null || end > siblings.length) end = siblings.length - return iterator.call(this, start, end) + while (++index < this.delayed.length) { + this.delayed[index].call(this) + if (this.state.one && this.state.found) break } - } -} -function createTypeIndex() { - var counts = {} + return this + } - index.count = count - index.nodes = 0 + /** + * @param {number|null|undefined} [start] + * @param {number|null|undefined} [end] + * @returns {[number, number]} + */ + defaults(start, end) { + if (start === null || start === undefined || start < 0) start = 0 + if (end === null || end === undefined || end > this.parent.children.length) + end = this.parent.children.length + return [start, end] + } +} - return index +class TypeIndex { + constructor() { + /** @type {Object.} */ + this.counts = {} + /** @type {number} */ + this.nodes = 0 + } - function index(node) { + /** + * @param {Node} node + * @returns {number} + */ + index(node) { var type = node.type - index.nodes++ + this.nodes++ - if (!own.call(counts, type)) counts[type] = 0 + if (!own.call(this.counts, type)) this.counts[type] = 0 // Note: `++` is intended to be postfixed! - return counts[type]++ + return this.counts[type]++ } - function count(node) { - return counts[node.type] + /** + * @param {Node} node + * @returns {number|undefined} + */ + count(node) { + return this.counts[node.type] } } diff --git a/lib/parse.js b/lib/parse.js index 9ff8035..2ada1d6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,35 +1,51 @@ -'use strict' - -module.exports = parse - -var Parser = require('css-selector-parser').CssSelectorParser -var zwitch = require('zwitch') -var nthCheck = require('nth-check').default - -var nth = ['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'] - -var parser = new Parser() - -var compile = zwitch('type', { - handlers: { - selectors: selectors, - ruleSet: ruleSet, - rule: rule - } -}) +/** + * @typedef {import('./types.js').Selector} Selector + * @typedef {import('./types.js').Selectors} Selectors + * @typedef {import('./types.js').RuleSet} RuleSet + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').RulePseudo} RulePseudo + * @typedef {import('./types.js').RulePseudoNth} RulePseudoNth + */ + +import {CssSelectorParser} from 'css-selector-parser' +import fauxEsmNthCheck from 'nth-check' +import {zwitch} from 'zwitch' + +/** @type {import('nth-check').default} */ +// @ts-ignore +var nthCheck = fauxEsmNthCheck.default + +var nth = new Set([ + 'nth-child', + 'nth-last-child', + 'nth-of-type', + 'nth-last-of-type' +]) + +var parser = new CssSelectorParser() parser.registerAttrEqualityMods('~', '^', '$', '*') parser.registerSelectorPseudos('any', 'matches', 'not', 'has') parser.registerNestingOperators('>', '+', '~') -function parse(selector) { +var compile = zwitch('type', {handlers: {selectors, ruleSet, rule}}) + +/** + * @param {string} selector + * @returns {Selector} + */ +export function parse(selector) { if (typeof selector !== 'string') { throw new TypeError('Expected `string` as selector, not `' + selector + '`') } + // @ts-ignore types are wrong. return compile(parser.parse(selector)) } +/** + * @param {Selectors} query + */ function selectors(query) { var selectors = query.selectors var index = -1 @@ -41,20 +57,29 @@ function selectors(query) { return query } +/** + * @param {RuleSet} query + */ function ruleSet(query) { return rule(query.rule) } +/** + * @param {Rule} query + */ function rule(query) { var pseudos = query.pseudos || [] var index = -1 + /** @type {RulePseudo|RulePseudoNth} */ var pseudo while (++index < pseudos.length) { pseudo = pseudos[index] - if (nth.indexOf(pseudo.name) > -1) { + if (nth.has(pseudo.name)) { + // @ts-ignore Patch a non-primitive type. pseudo.value = nthCheck(pseudo.value) + // @ts-ignore Patch a non-primitive type. pseudo.valueType = 'function' } } diff --git a/lib/pseudo.js b/lib/pseudo.js index c1d5303..55f5536 100644 --- a/lib/pseudo.js +++ b/lib/pseudo.js @@ -1,52 +1,68 @@ -'use strict' - -module.exports = match - -var zwitch = require('zwitch') -var not = require('not') -var convert = require('unist-util-is/convert') +/** + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').RulePseudo} RulePseudo + * @typedef {import('./types.js').RulePseudoNth} RulePseudoNth + * @typedef {import('./types.js').RulePseudoSelector} RulePseudoSelector + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').Selector} Selector + * @typedef {import('./types.js').Selectors} Selectors + * @typedef {import('./types.js').SelectState} SelectState + * @typedef {import('./types.js').Node} Node + */ + +import {zwitch} from 'zwitch' +import {convert} from 'unist-util-is' +import {parent} from './util.js' var is = convert() -match.needsIndex = [ - 'first-child', - 'first-of-type', - 'last-child', - 'last-of-type', - 'nth-child', - 'nth-last-child', - 'nth-of-type', - 'nth-last-of-type', - 'only-child', - 'only-of-type' -] - var handle = zwitch('name', { unknown: unknownPseudo, invalid: invalidPseudo, handlers: { any: matches, blank: empty, - empty: empty, + empty, 'first-child': firstChild, 'first-of-type': firstOfType, has: hasSelector, 'last-child': lastChild, 'last-of-type': lastOfType, - matches: matches, - not: not(matches), + matches, + not, 'nth-child': nthChild, 'nth-last-child': nthLastChild, 'nth-of-type': nthOfType, 'nth-last-of-type': nthLastOfType, 'only-child': onlyChild, 'only-of-type': onlyOfType, - root: root, - scope: scope + root, + scope } }) -function match(query, node, index, parent, state) { +pseudo.needsIndex = [ + 'first-child', + 'first-of-type', + 'last-child', + 'last-of-type', + 'nth-child', + 'nth-last-child', + 'nth-of-type', + 'nth-last-of-type', + 'only-child', + 'only-of-type' +] + +/** + * @param {Rule} query + * @param {Node} node + * @param {number|null} index + * @param {Parent|null} parent + * @param {SelectState} state + * @returns {boolean} + */ +export function pseudo(query, node, index, parent, state) { var pseudos = query.pseudos var offset = -1 @@ -57,16 +73,24 @@ function match(query, node, index, parent, state) { return true } -function matches(query, node, index, parent, state) { +/** + * @param {RulePseudoSelector} query + * @param {Node} node + * @param {number|null} _1 + * @param {Parent|null} _2 + * @param {SelectState} state + * @returns {boolean} + */ +function matches(query, node, _1, _2, state) { var shallow = state.shallow var one = state.one - var anything = state.any + /** @type {boolean} */ var result state.one = true state.shallow = true - result = anything(query.value, node, state)[0] === node + result = state.any(query.value, node, state)[0] === node state.shallow = shallow state.one = one @@ -74,73 +98,190 @@ function matches(query, node, index, parent, state) { return result } -function root(query, node, index, parent) { +/** + * @param {RulePseudoSelector} query + * @param {Node} node + * @param {number|null} index + * @param {Parent|null} parent + * @param {SelectState} state + * @returns {boolean} + */ +function not(query, node, index, parent, state) { + return !matches(query, node, index, parent, state) +} + +/** + * @param {RulePseudo} _1 + * @param {Node} node + * @param {number|null} _2 + * @param {Parent|null} parent + * @returns {boolean} + */ +function root(_1, node, _2, parent) { return is(node) && !parent } -function scope(query, node, index, parent, state) { - return is(node) && state.scopeNodes.indexOf(node) > -1 +/** + * @param {RulePseudo} _1 + * @param {Node} node + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function scope(_1, node, _2, _3, state) { + return is(node) && state.scopeNodes.includes(node) } -function empty(query, node) { - return node.children ? !node.children.length : !('value' in node) +/** + * @param {RulePseudo} _1 + * @param {Node} node + * @returns {boolean} + */ +function empty(_1, node) { + return parent(node) ? node.children.length === 0 : !('value' in node) } -function firstChild(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function firstChild(query, _1, _2, _3, state) { assertDeep(state, query) return state.nodeIndex === 0 // Specifically `0`, not falsey. } -function lastChild(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function lastChild(query, _1, _2, _3, state) { assertDeep(state, query) return state.nodeIndex === state.nodeCount - 1 } -function onlyChild(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function onlyChild(query, _1, _2, _3, state) { assertDeep(state, query) return state.nodeCount === 1 } -function nthChild(query, node, index, parent, state) { +/** + * @param {RulePseudoNth} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function nthChild(query, _1, _2, _3, state) { assertDeep(state, query) return query.value(state.nodeIndex) } -function nthLastChild(query, node, index, parent, state) { +/** + * @param {RulePseudoNth} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function nthLastChild(query, _1, _2, _3, state) { assertDeep(state, query) return query.value(state.nodeCount - state.nodeIndex - 1) } -function nthOfType(query, node, index, parent, state) { +/** + * @param {RulePseudoNth} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function nthOfType(query, _1, _2, _3, state) { assertDeep(state, query) return query.value(state.typeIndex) } -function nthLastOfType(query, node, index, parent, state) { +/** + * @param {RulePseudoNth} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function nthLastOfType(query, _1, _2, _3, state) { assertDeep(state, query) return query.value(state.typeCount - 1 - state.typeIndex) } -function firstOfType(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function firstOfType(query, _1, _2, _3, state) { assertDeep(state, query) return state.typeIndex === 0 } -function lastOfType(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function lastOfType(query, _1, _2, _3, state) { assertDeep(state, query) return state.typeIndex === state.typeCount - 1 } -function onlyOfType(query, node, index, parent, state) { +/** + * @param {RulePseudo} query + * @param {Node} _1 + * @param {number|null} _2 + * @param {Parent|null} _3 + * @param {SelectState} state + * @returns {boolean} + */ +function onlyOfType(query, _1, _2, _3, state) { assertDeep(state, query) return state.typeCount === 1 } -/* istanbul ignore next - Shouldn’t be invoked, parser gives correct data. */ +// Shouldn’t be invoked, parser gives correct data. +/* c8 ignore next 3 */ function invalidPseudo() { throw new Error('Invalid pseudo-selector') } +/** + * @param {RulePseudo} query + * @returns {boolean} + */ function unknownPseudo(query) { if (query.name) { throw new Error('Unknown pseudo-selector `' + query.name + '`') @@ -149,25 +290,38 @@ function unknownPseudo(query) { throw new Error('Unexpected pseudo-element or empty pseudo-class') } +/** + * @param {SelectState} state + * @param {RulePseudo|RulePseudoNth} query + */ function assertDeep(state, query) { if (state.shallow) { throw new Error('Cannot use `:' + query.name + '` without parent') } } -function hasSelector(query, node, index, parent, state) { +/** + * @param {RulePseudoSelector} query + * @param {Node} node + * @param {number|null} _1 + * @param {Parent|null} _2 + * @param {SelectState} state + * @returns {boolean} + */ +function hasSelector(query, node, _1, _2, state) { var shallow = state.shallow var one = state.one var scopeNodes = state.scopeNodes var value = appendScope(query.value) var anything = state.any + /** @type {boolean} */ var result state.shallow = false state.one = true state.scopeNodes = [node] - result = anything(value, node, state)[0] + result = Boolean(anything(value, node, state)[0]) state.shallow = shallow state.one = one @@ -176,26 +330,37 @@ function hasSelector(query, node, index, parent, state) { return result } +/** + * @param {Selector} value + */ function appendScope(value) { + /** @type {Selectors} */ var selector = value.type === 'ruleSet' ? {type: 'selectors', selectors: [value]} : value var index = -1 + /** @type {Rule} */ var rule while (++index < selector.selectors.length) { rule = selector.selectors[index].rule rule.nestingOperator = null - /* istanbul ignore else - needed if new pseudo’s are added that accepts commas (such as, `:lang(en, nl)`) */ + // Needed if new pseudo’s are added that accepts commas (such as + // `:lang(en, nl)`) + /* c8 ignore else */ if ( !rule.pseudos || rule.pseudos.length !== 1 || rule.pseudos[0].name !== 'scope' ) { selector.selectors[index] = { - type: 'rule', - rule: rule, - pseudos: [{name: 'scope'}] + type: 'ruleSet', + rule: { + type: 'rule', + rule, + // @ts-ignore pseudos are fine w/ just a name! + pseudos: [{name: 'scope'}] + } } } } diff --git a/lib/test.js b/lib/test.js index fc9c766..18f6938 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,24 +1,30 @@ -'use strict' +/** + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').SelectState} SelectState + */ -module.exports = test +import {attribute} from './attribute.js' +import {name} from './name.js' +import {pseudo} from './pseudo.js' -var name = require('./name') -var attributes = require('./attribute') -var pseudos = require('./pseudo') - -function test(query, node, index, parent, state) { - if (query.id) { - throw new Error('Invalid selector: id') - } - - if (query.classNames) { - throw new Error('Invalid selector: class') - } +/** + * @param {Rule} query + * @param {Node} node + * @param {number|null} index + * @param {Parent|null} parent + * @param {SelectState} state + * @returns {boolean} + */ +export function test(query, node, index, parent, state) { + if (query.id) throw new Error('Invalid selector: id') + if (query.classNames) throw new Error('Invalid selector: class') return Boolean( node && (!query.tagName || name(query, node)) && - (!query.attrs || attributes(query, node)) && - (!query.pseudos || pseudos(query, node, index, parent, state)) + (!query.attrs || attribute(query, node)) && + (!query.pseudos || pseudo(query, node, index, parent, state)) ) } diff --git a/lib/types.js b/lib/types.js new file mode 100644 index 0000000..1d5fb2f --- /dev/null +++ b/lib/types.js @@ -0,0 +1,61 @@ +/** + * @typedef {import('css-selector-parser').Selector} Selector + * @typedef {import('css-selector-parser').Selectors} Selectors + * @typedef {import('css-selector-parser').RuleSet} RuleSet + * @typedef {import('css-selector-parser').Rule} Rule + * @typedef {import('css-selector-parser').RulePseudo} RulePseudo + * @typedef {import('css-selector-parser').AttrValueType} AttrValueType + * @typedef {Selector|Rule|RulePseudo} Query + * + * Fix for types. + * @typedef {Object} RuleAttr + * @property {string} name + * @property {string} [operator] + * @property {AttrValueType} [valueType] + * @property {string} [value] + * + * More specific type for registered selector pseudos. + * @typedef {Object} RulePseudoSelector + * @property {string} name + * @property {'selector'} valueType + * @property {Selector} value + * + * Overwrite to compile nth-checks once. + * @typedef {Object} RulePseudoNth + * @property {string} name + * @property {'function'} valueType + * @property {(index: number) => boolean} value + * + * @typedef {import('unist').Node} Node + * @typedef {import('unist').Parent} Parent + * + * @typedef {Object} SelectState + * @property {(query: Query, node: Node, state: SelectState) => Node[]} any + * @property {Array.} [scopeNodes] + * @property {SelectIterator} [iterator] + * @property {boolean} [one=false] + * @property {boolean} [shallow=false] + * @property {boolean} [index=false] + * @property {boolean} [found=false] + * @property {number} [typeIndex] Track siblings + * @property {number} [nodeIndex] Track siblings + * @property {number} [typeCount] Track siblings + * @property {number} [nodeCount] Track siblings + */ + +/** + * @callback SelectIterator + * @param {Rule} query + * @param {Node} node + * @param {number} index + * @param {Parent|null} parent + * @param {SelectState} state + */ + +/** + * @typedef {( + * ((query: Rule, node: Node, index: number|null, parent: Parent|null, state: SelectState) => void) + * )} Handler + */ + +export {} diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..49a92ff --- /dev/null +++ b/lib/util.js @@ -0,0 +1,33 @@ +/** + * @typedef {import('./types.js').Selector} Selector + * @typedef {import('./types.js').Selectors} Selectors + * @typedef {import('./types.js').Rule} Rule + * @typedef {import('./types.js').RuleSet} RuleSet + * @typedef {import('./types.js').RulePseudo} RulePseudo + * @typedef {import('./types.js').Query} Query + * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').SelectIterator} SelectIterator + * @typedef {import('./types.js').SelectState} SelectState + */ + +/** + * @param {Node} node + * @returns {node is Parent} + */ +export function root(node) { + return ( + // Root in nlcst. + node.type === 'RootNode' || + // Rest + node.type === 'root' + ) +} + +/** + * @param {Node} node + * @returns {node is Parent} + */ +export function parent(node) { + return Array.isArray(node.children) +} diff --git a/package.json b/package.json index 093baf4..25dd667 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unist-util-select", - "version": "3.0.4", + "version": "4.0.0", "description": "unist utility to select nodes with CSS-like selectors", "license": "MIT", "keywords": [ @@ -38,41 +38,41 @@ "Titus Wormer (https://wooorm.com)", "Christian Murphy " ], + "sideEffects": false, + "type": "module", + "main": "index.js", + "types": "index.d.ts", "files": [ - "index.js", "lib/", - "types/index.d.ts" + "index.d.ts", + "index.js" ], - "types": "types/index.d.ts", "dependencies": { "css-selector-parser": "^1.0.0", - "not": "^0.1.0", "nth-check": "^2.0.0", - "unist-util-is": "^4.0.0", - "zwitch": "^1.0.0" + "unist-util-is": "^5.0.0", + "zwitch": "^2.0.0" }, "devDependencies": { - "dtslint": "^4.0.0", - "nyc": "^15.0.0", + "@types/tape": "^4.0.0", + "c8": "^7.0.0", "prettier": "^2.0.0", "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", + "rimraf": "^3.0.0", "tape": "^5.0.0", - "unist-builder": "^2.0.0", - "xo": "^0.36.0" + "type-coverage": "^2.0.0", + "typescript": "^4.0.0", + "unist-builder": "^3.0.0", + "xo": "^0.38.0" }, "scripts": { + "prepack": "npm run build && npm run format", + "build": "rimraf \"{lib/**,test/**,}*.d.ts\" && tsc && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test", - "test-coverage": "nyc --reporter lcov tape test/index.js", - "test-types": "dtslint types", - "test": "npm run format && npm run test-coverage && npm run test-types" - }, - "nyc": { - "check-coverage": true, - "lines": 100, - "functions": 100, - "branches": 100 + "test-api": "node test/index.js", + "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test/index.js", + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { "tabWidth": 2, @@ -84,28 +84,21 @@ }, "xo": { "prettier": true, - "esnext": false, - "ignore": [ - "types" - ], "rules": { - "eqeqeq": [ - "error", - "always", - { - "null": "ignore" - } - ], - "no-eq-null": "off", + "no-var": "off", + "prefer-arrow-callback": "off", "max-params": "off", - "unicorn/explicit-length-check": "off", - "unicorn/prefer-includes": "off", - "unicorn/prefer-reflect-apply": "off" + "unicorn/no-array-for-each": "off" } }, "remarkConfig": { "plugins": [ "preset-wooorm" ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true } } diff --git a/readme.md b/readme.md index a505013..ed5856c 100644 --- a/readme.md +++ b/readme.md @@ -19,6 +19,9 @@ This information is not stored in unist, so selectors like that don’t work. ## Install +This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): +Node 12+ is needed to use it and it must be `import`ed instead of `require`d. + [npm][]: ```sh @@ -27,7 +30,10 @@ npm install unist-util-select ## API -### `select.matches(selector, node)` +This package exports the following identifiers: `matches`, `select`, `selectAll`. +There is no default export. + +### `matches(selector, node)` Check that the given [node][] matches `selector`. Returns `boolean`, whether the node matches or not. @@ -38,22 +44,22 @@ Thus, nesting in selectors is not supported (`paragraph strong`, This only checks that the given element matches the selector. ```js -var u = require('unist-builder') -var matches = require('unist-util-select').matches +import {u} from 'unist-builder' +import {matches} from 'unist-util-select' matches('strong, em', u('strong', [u('text', 'important')])) // => true matches('[lang]', u('code', {lang: 'js'}, 'console.log(1)')) // => true ``` -### `select.select(selector, tree)` +### `select(selector, tree)` Select the first node matching `selector` in the given `tree` (could be the tree itself). Returns the found [node][], if any. ```js -var u = require('unist-builder') -var select = require('unist-util-select').select +import {u} from 'unist-builder' +import {select} from 'unist-util-select' console.log( select( @@ -75,15 +81,15 @@ Yields: {type: 'paragraph', children: [{type: 'text', value: 'Delta'}]} ``` -### `select.selectAll(selector, tree)` +### `selectAll(selector, tree)` Select all nodes matching `selector` in the given `tree` (could include the tree itself). Returns all found [node][]s, if any. ```js -var u = require('unist-builder') -var selectAll = require('unist-util-select').selectAll +import {u} from 'unist-builder' +import {selectAll} from 'unist-util-select' console.log( selectAll( diff --git a/test/all.js b/test/all.js index 686d70e..7d6ce9c 100644 --- a/test/all.js +++ b/test/all.js @@ -1,8 +1,6 @@ -'use strict' - -var test = require('tape') -var u = require('unist-builder') -var selectAll = require('..').selectAll +import test from 'tape' +import {u} from 'unist-builder' +import {selectAll} from '../index.js' test('all together now', function (t) { t.deepEqual( diff --git a/test/index.js b/test/index.js index 3cbe1ad..d5e2b4f 100644 --- a/test/index.js +++ b/test/index.js @@ -1,8 +1,6 @@ -'use strict' - /* eslint-disable import/no-unassigned-import */ -require('./matches') -require('./select') -require('./select-all') -require('./all') +import './matches.js' +import './select.js' +import './select-all.js' +import './all.js' /* eslint-enable import/no-unassigned-import */ diff --git a/test/matches.js b/test/matches.js index 083ea7b..9c79128 100644 --- a/test/matches.js +++ b/test/matches.js @@ -1,13 +1,12 @@ -'use strict' - -var test = require('tape') -var u = require('unist-builder') -var matches = require('..').matches +import test from 'tape' +import {u} from 'unist-builder' +import {matches} from '../index.js' test('select.matches()', function (t) { t.test('invalid selector', function (st) { st.throws( function () { + // @ts-ignore runtime. matches() }, /Error: Expected `string` as selector, not `undefined`/, @@ -16,6 +15,7 @@ test('select.matches()', function (t) { st.throws( function () { + // @ts-ignore runtime. matches([], u('root', [])) }, /Error: Expected `string` as selector, not ``/, diff --git a/test/select-all.js b/test/select-all.js index d280743..7b67e54 100644 --- a/test/select-all.js +++ b/test/select-all.js @@ -1,13 +1,12 @@ -'use strict' - -var test = require('tape') -var u = require('unist-builder') -var selectAll = require('..').selectAll +import test from 'tape' +import {u} from 'unist-builder' +import {selectAll} from '../index.js' test('select.selectAll()', function (t) { t.test('invalid selectors', function (st) { st.throws( function () { + // @ts-ignore runtime. selectAll() }, /Error: Expected `string` as selector, not `undefined`/, @@ -16,6 +15,7 @@ test('select.selectAll()', function (t) { st.throws( function () { + // @ts-ignore runtime. selectAll([], u('a')) }, /Error: Expected `string` as selector, not ``/, diff --git a/test/select.js b/test/select.js index 881fce4..0307688 100644 --- a/test/select.js +++ b/test/select.js @@ -1,13 +1,12 @@ -'use strict' - -var test = require('tape') -var u = require('unist-builder') -var select = require('..').select +import test from 'tape' +import {u} from 'unist-builder' +import {select} from '../index.js' test('select.select()', function (t) { t.test('invalid selectors', function (st) { st.throws( function () { + // @ts-ignore runtime. select() }, /Error: Expected `string` as selector, not `undefined`/, @@ -16,6 +15,7 @@ test('select.select()', function (t) { st.throws( function () { + // @ts-ignore runtime. select([], u('a')) }, /Error: Expected `string` as selector, not ``/, @@ -133,7 +133,7 @@ test('select.select()', function (t) { select( 'b > b', u('a', [ - u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, u('b', {x: 4}))]) + u('b', {x: 1}, [u('b', {x: 2}), u('b', {x: 3}, [u('b', {x: 4})])]) ]) ), u('b', {x: 2}), diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2b103bd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["*.js", "lib/**/*.js", "test/**/*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + } +} diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 7aae6cf..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// TypeScript Version: 3.5 - -import {Node} from 'unist' - -/** - * Is there a match for the given selector in the Unist tree - * - * @param selector CSS-like selector - * @param tree Unist node or tree of nodes to search - */ -declare function matches(selector: string, tree: Node): boolean - -/** - * Find first Node that matches selector - * - * @param selector CSS-like selector - * @param tree Unist node or tree of nodes to search - */ -declare function select(selector: string, tree: Node): Node | null - -/** - * Find all Nodes that match selector - * - * @param selector CSS-like selector - * @param tree Unist node or tree of nodes to search - */ -declare function selectAll(selector: string, tree: Node): Node[] - -export {matches, select, selectAll} diff --git a/types/tsconfig.json b/types/tsconfig.json deleted file mode 100644 index 9d2d1d3..0000000 --- a/types/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "strict": true, - "baseUrl": ".", - "paths": { - "unist-util-select": ["index.d.ts"] - } - } -} diff --git a/types/tslint.json b/types/tslint.json deleted file mode 100644 index 6978386..0000000 --- a/types/tslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "whitespace": false, - "semicolon": false - } -} diff --git a/types/unist-util-select-tests.ts b/types/unist-util-select-tests.ts deleted file mode 100644 index 85a1b7e..0000000 --- a/types/unist-util-select-tests.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {matches, select, selectAll} from 'unist-util-select' - -matches() // $ExpectError -matches('*') // $ExpectError -matches('*', {type: 'root'}) // $ExpectType boolean - -select() // $ExpectError -select('*') // $ExpectError -select('*', {type: 'root'}) // $ExpectType Node | null - -selectAll() // $ExpectError -selectAll('*') // $ExpectError -selectAll('*', {type: 'root'}) // $ExpectType Node[]