From a2b65a367c3b1afee93aa1a2d47277b283961449 Mon Sep 17 00:00:00 2001 From: Titus Date: Mon, 16 Jan 2023 16:30:11 +0100 Subject: [PATCH 01/12] Use Node 16 in Actions Signed-off-by: Titus --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee318ca..fb63387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/hydrogen + - lts/gallium - node From 20830dfb7a00962daf9fc075082a842e2f45def4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 14:29:00 +0200 Subject: [PATCH 02/12] Update dev-dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a4a6027..ef7086a 100644 --- a/package.json +++ b/package.json @@ -41,20 +41,20 @@ "hastscript": "^7.0.0" }, "devDependencies": { - "@types/node": "^18.0.0", - "c8": "^7.0.0", - "prettier": "^2.0.0", + "@types/node": "^20.0.0", + "c8": "^8.0.0", + "prettier": "^3.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", + "typescript": "^5.0.0", "unist-builder": "^3.0.0", - "xo": "^0.53.0" + "xo": "^0.55.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && type-coverage", - "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", + "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", "test-api": "node --conditions development test.js", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run format && npm run test-coverage" From f0249adaebef3b576d13e87301e1eeacdcb16ca4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 14:29:48 +0200 Subject: [PATCH 03/12] Update `@types/hast`, utilities --- lib/index.js | 1 + package.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index a13e81a..a33372c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -32,6 +32,7 @@ parser.registerNestingOperators('>', '+', '~') // Register these so we can throw nicer errors. parser.registerAttrEqualityMods('~', '|', '^', '$', '*') +// To do: remove `space` shortcut. /** * Create one or more `Element`s from a CSS selector. * diff --git a/package.json b/package.json index ef7086a..3ee15d6 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "index.js" ], "dependencies": { - "@types/hast": "^2.0.0", + "@types/hast": "^3.0.0", "css-selector-parser": "^1.0.0", - "hastscript": "^7.0.0" + "hastscript": "^8.0.0" }, "devDependencies": { "@types/node": "^20.0.0", @@ -48,7 +48,7 @@ "remark-preset-wooorm": "^9.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "unist-builder": "^3.0.0", + "unist-builder": "^4.0.0", "xo": "^0.55.0" }, "scripts": { From ebc49d03708c7725af7c37edcd19438efdde2e2f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:02:45 +0200 Subject: [PATCH 04/12] Update to match CSS selectors 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates `css-selector-parser`, which is completely different. You’ll get some different errors if you do weird things. Otherwise, this changes: * no longer supports whitespace only, either pass an empty string or an actual selector * change to remove invalid attribute selectors such as `[a=b,c]`, use `[a="b,c"]` instead --- lib/index.js | 133 ++++++++++++++++++++++++++------------------------- package.json | 8 +++- test.js | 22 ++++----- 3 files changed, 86 insertions(+), 77 deletions(-) diff --git a/lib/index.js b/lib/index.js index a33372c..50d469f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,6 @@ /** - * @typedef {import('css-selector-parser').RuleAttr} RuleAttr - * @typedef {import('css-selector-parser').RulePseudo} RulePseudo - * @typedef {import('css-selector-parser').Rule} Rule + * @typedef {import('css-selector-parser').AstAttribute} AstAttribute + * @typedef {import('css-selector-parser').AstRule} AstRule * * @typedef {import('hast').Element} HastElement * @typedef {import('hast').Properties} HastProperties @@ -23,14 +22,11 @@ * Current space. */ +import {ok as assert} from 'devlop' import {h, s} from 'hastscript' -import {CssSelectorParser} from 'css-selector-parser' +import {createParser} from 'css-selector-parser' -const parser = new CssSelectorParser() - -parser.registerNestingOperators('>', '+', '~') -// Register these so we can throw nicer errors. -parser.registerAttrEqualityMods('~', '|', '^', '$', '*') +const cssSelectorParse = createParser({syntax: 'selectors-4'}) // To do: remove `space` shortcut. /** @@ -52,34 +48,35 @@ export function fromSelector(selector, space) { 'html' } - const query = parser.parse(selector || '') + const query = cssSelectorParse(selector || '*') - if (query && query.type === 'selectors') { + if (query.rules.length > 1) { throw new Error('Cannot handle selector list') } - const result = query ? rule(query.rule, state) : [] + const head = query.rules[0] + assert(head, 'expected rule') if ( - query && - query.rule.rule && - (query.rule.rule.nestingOperator === '+' || - query.rule.rule.nestingOperator === '~') + head.nestedRule && + (head.nestedRule.combinator === '+' || head.nestedRule.combinator === '~') ) { throw new Error( 'Cannot handle sibling combinator `' + - query.rule.rule.nestingOperator + + head.nestedRule.combinator + '` at root' ) } - return result[0] || build(state.space)('') + const result = rule(head, state) + + return result[0] } /** * Turn a rule into one or more elements. * - * @param {Rule} query + * @param {AstRule} query * Selector. * @param {State} state * Info on current context. @@ -88,82 +85,90 @@ export function fromSelector(selector, space) { */ function rule(query, state) { const space = - state.space === 'html' && query.tagName === 'svg' ? 'svg' : state.space + state.space === 'html' && + query.tag && + query.tag.type === 'TagName' && + query.tag.name === 'svg' + ? 'svg' + : state.space + + const pseudoClass = query.pseudoClasses ? query.pseudoClasses[0] : undefined + + if (pseudoClass) { + if (pseudoClass.name) { + throw new Error('Cannot handle pseudo class `' + pseudoClass.name + '`') + /* c8 ignore next 4 -- types say this can occur, but I don’t understand how */ + } + + throw new Error('Cannot handle empty pseudo class') + } - checkPseudos(query.pseudos || []) + if (query.pseudoElement) { + throw new Error( + 'Cannot handle pseudo element `' + query.pseudoElement + '`' + ) + } - const node = build(space)(query.tagName === '*' ? '' : query.tagName || '', { - id: query.id, + const name = query.tag && query.tag.type === 'TagName' ? query.tag.name : '' + + const node = build(space)(name, { + id: query.ids ? query.ids[query.ids.length - 1] : undefined, className: query.classNames, - ...attrsToHast(query.attrs || []) + ...attributesToHast(query.attributes) }) const results = [node] - if (query.rule) { + if (query.nestedRule) { // Sibling. if ( - query.rule.nestingOperator === '+' || - query.rule.nestingOperator === '~' + query.nestedRule.combinator === '+' || + query.nestedRule.combinator === '~' ) { - results.push(...rule(query.rule, state)) + results.push(...rule(query.nestedRule, state)) } // Descendant. else { - node.children.push(...rule(query.rule, {space})) + node.children.push(...rule(query.nestedRule, {space})) } } return results } -/** - * Check pseudo selectors. - * - * @param {Array} pseudos - * Pseudo selectors. - * @returns {void} - * Nothing. - * @throws {Error} - * When a pseudo is defined. - */ -function checkPseudos(pseudos) { - const pseudo = pseudos[0] - - if (pseudo) { - if (pseudo.name) { - throw new Error('Cannot handle pseudo-selector `' + pseudo.name + '`') - } - - throw new Error('Cannot handle pseudo-element or empty pseudo-class') - } -} - /** * Turn attribute selectors into properties. * - * @param {Array} attrs + * @param {Array | undefined} attributes * Attribute selectors. * @returns {HastProperties} * Properties. */ -function attrsToHast(attrs) { +function attributesToHast(attributes) { /** @type {HastProperties} */ const props = {} let index = -1 - while (++index < attrs.length) { - const attr = attrs[index] - - if ('operator' in attr) { - if (attr.operator === '=') { - props[attr.name] = attr.value + if (attributes) { + while (++index < attributes.length) { + const attr = attributes[index] + + if ('operator' in attr) { + if (attr.operator === '=') { + const value = attr.value + + // eslint-disable-next-line max-depth + if (value) { + assert(value.type === 'String', 'substitution are not enabled') + props[attr.name] = value.value + } + } else { + throw new Error( + 'Cannot handle attribute equality modifier `' + attr.operator + '`' + ) + } } else { - throw new Error( - 'Cannot handle attribute equality modifier `' + attr.operator + '`' - ) + props[attr.name] = true } - } else { - props[attr.name] = true } } diff --git a/package.json b/package.json index 3ee15d6..d562992 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ ], "dependencies": { "@types/hast": "^3.0.0", - "css-selector-parser": "^1.0.0", + "css-selector-parser": "^2.0.0", + "devlop": "^1.0.0", "hastscript": "^8.0.0" }, "devDependencies": { @@ -68,7 +69,10 @@ "trailingComma": "none" }, "xo": { - "prettier": true + "prettier": true, + "rules": { + "unicorn/prefer-at": "off" + } }, "remarkConfig": { "plugins": [ diff --git a/test.js b/test.js index ddea49d..b28bb70 100644 --- a/test.js +++ b/test.js @@ -15,7 +15,7 @@ test('fromSelector()', () => { () => { fromSelector('@supports (transform-origin: 5% 5%) {}') }, - /Error: Rule expected but "@" found/, + /Expected rule but "@" found/, 'should throw w/ invalid selector' ) @@ -23,7 +23,7 @@ test('fromSelector()', () => { () => { fromSelector('a, b') }, - /Error: Cannot handle selector list/, + /Cannot handle selector list/, 'should throw w/ multiple selector' ) @@ -31,7 +31,7 @@ test('fromSelector()', () => { () => { fromSelector('a + b') }, - /Error: Cannot handle sibling combinator `\+` at root/, + /Cannot handle sibling combinator `\+` at root/, 'should throw w/ next-sibling combinator at root' ) @@ -39,7 +39,7 @@ test('fromSelector()', () => { () => { fromSelector('a ~ b') }, - /Error: Cannot handle sibling combinator `~` at root/, + /Cannot handle sibling combinator `~` at root/, 'should throw w/ subsequent-sibling combinator at root' ) @@ -47,7 +47,7 @@ test('fromSelector()', () => { () => { fromSelector('[foo%=bar]') }, - /Error: Expected "=" but "%" found./, + /Expected a valid attribute selector operator/, 'should throw w/ attribute modifiers' ) @@ -55,7 +55,7 @@ test('fromSelector()', () => { () => { fromSelector('[foo~=bar]') }, - /Error: Cannot handle attribute equality modifier `~=`/, + /Cannot handle attribute equality modifier `~=`/, 'should throw w/ attribute modifiers' ) @@ -63,7 +63,7 @@ test('fromSelector()', () => { () => { fromSelector(':active') }, - /Error: Cannot handle pseudo-selector `active`/, + /Cannot handle pseudo class `active`/, 'should throw on pseudo classes' ) @@ -71,7 +71,7 @@ test('fromSelector()', () => { () => { fromSelector(':nth-foo(2n+1)') }, - /Error: Cannot handle pseudo-selector `nth-foo`/, + /Unknown pseudo-class/, 'should throw on pseudo class “functions”' ) @@ -79,13 +79,13 @@ test('fromSelector()', () => { () => { fromSelector('::before') }, - /Error: Cannot handle pseudo-element or empty pseudo-class/, + /Cannot handle pseudo element `before`/, 'should throw on invalid pseudo elements' ) assert.deepEqual(fromSelector(), h(''), 'should support no selector') assert.deepEqual(fromSelector(''), h(''), 'should support the empty string') - assert.deepEqual(fromSelector(' '), h(''), 'should support whitespace only') + assert.deepEqual( fromSelector('*'), h(''), @@ -184,7 +184,7 @@ test('fromSelector()', () => { assert.deepEqual( fromSelector( - 'p svg[viewbox=0 0 10 10] circle[cx=10][cy=10][r=10] altGlyph' + 'p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10] altGlyph' ), h('p', [ s('svg', {viewBox: '0 0 10 10'}, [ From c15a6445d6f1034feca598ac20be9c2a20d2b5c4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:10:44 +0200 Subject: [PATCH 05/12] Refactor code-style --- lib/index.js | 26 ++-- test.js | 332 +++++++++++++++++++++++++-------------------------- 2 files changed, 175 insertions(+), 183 deletions(-) diff --git a/lib/index.js b/lib/index.js index 50d469f..8ead20a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,27 +4,29 @@ * * @typedef {import('hast').Element} HastElement * @typedef {import('hast').Properties} HastProperties - * - * @typedef {'html' | 'svg'} Space - * Name of namespace. - * + */ + +/** * @typedef Options * Configuration. * @property {Space} [space] - * Which space first element in the selector is in. + * Which space first element in the selector is in (default: `'html'`). * * When an `svg` element is created in HTML, the space is automatically * switched to SVG. * + * @typedef {'html' | 'svg'} Space + * Name of namespace. + * * @typedef State * Info on current context. * @property {Space} space * Current space. */ +import {createParser} from 'css-selector-parser' import {ok as assert} from 'devlop' import {h, s} from 'hastscript' -import {createParser} from 'css-selector-parser' const cssSelectorParse = createParser({syntax: 'selectors-4'}) @@ -33,18 +35,18 @@ const cssSelectorParse = createParser({syntax: 'selectors-4'}) * Create one or more `Element`s from a CSS selector. * * @param {string | null | undefined} [selector=''] - * CSS selector. - * @param {Space | Options | null | undefined} [space='html'] - * Space or configuration. + * CSS selector (default: `''`). + * @param {Options | Space | null | undefined} [options] + * Configuration (optional). * @returns {HastElement} * Built tree. */ -export function fromSelector(selector, space) { +export function fromSelector(selector, options) { /** @type {State} */ const state = { space: - (space && typeof space === 'object' && space.space) || - (typeof space === 'string' && space) || + (options && typeof options === 'object' && options.space) || + (typeof options === 'string' && options) || 'html' } diff --git a/test.js b/test.js index b28bb70..9d4b18e 100644 --- a/test.js +++ b/test.js @@ -2,195 +2,185 @@ import assert from 'node:assert/strict' import test from 'node:test' import {h, s} from 'hastscript' import {fromSelector} from './index.js' -import * as mod from './index.js' -test('fromSelector()', () => { - assert.deepEqual( - Object.keys(mod).sort(), - ['fromSelector'], - 'should expose the public api' - ) +test('fromSelector', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ + 'fromSelector' + ]) + }) - assert.throws( - () => { + await t.test('should throw w/ invalid selector', async function () { + assert.throws(function () { fromSelector('@supports (transform-origin: 5% 5%) {}') - }, - /Expected rule but "@" found/, - 'should throw w/ invalid selector' - ) + }, /Expected rule but "@" found/) + }) - assert.throws( - () => { + await t.test('should throw w/ multiple selector', async function () { + assert.throws(function () { fromSelector('a, b') - }, - /Cannot handle selector list/, - 'should throw w/ multiple selector' - ) + }, /Cannot handle selector list/) + }) - assert.throws( - () => { - fromSelector('a + b') - }, - /Cannot handle sibling combinator `\+` at root/, - 'should throw w/ next-sibling combinator at root' + await t.test( + 'should throw w/ next-sibling combinator at root', + async function () { + assert.throws(function () { + fromSelector('a + b') + }, /Cannot handle sibling combinator `\+` at root/) + } ) - assert.throws( - () => { - fromSelector('a ~ b') - }, - /Cannot handle sibling combinator `~` at root/, - 'should throw w/ subsequent-sibling combinator at root' + await t.test( + 'should throw w/ subsequent-sibling combinator at root', + async function () { + assert.throws(function () { + fromSelector('a ~ b') + }, /Cannot handle sibling combinator `~` at root/) + } ) - assert.throws( - () => { + await t.test('should throw w/ attribute modifiers', async function () { + assert.throws(function () { fromSelector('[foo%=bar]') - }, - /Expected a valid attribute selector operator/, - 'should throw w/ attribute modifiers' - ) + }, /Expected a valid attribute selector operator/) + }) - assert.throws( - () => { + await t.test('should throw w/ attribute modifiers', async function () { + assert.throws(function () { fromSelector('[foo~=bar]') - }, - /Cannot handle attribute equality modifier `~=`/, - 'should throw w/ attribute modifiers' - ) + }, /Cannot handle attribute equality modifier `~=`/) + }) - assert.throws( - () => { + await t.test('should throw on pseudo classes', async function () { + assert.throws(function () { fromSelector(':active') - }, - /Cannot handle pseudo class `active`/, - 'should throw on pseudo classes' - ) + }, /Cannot handle pseudo class `active`/) + }) - assert.throws( - () => { + await t.test('should throw on pseudo class “functions”', async function () { + assert.throws(function () { fromSelector(':nth-foo(2n+1)') - }, - /Unknown pseudo-class/, - 'should throw on pseudo class “functions”' - ) + }, /Unknown pseudo-class/) + }) - assert.throws( - () => { + await t.test('should throw on invalid pseudo elements', async function () { + assert.throws(function () { fromSelector('::before') - }, - /Cannot handle pseudo element `before`/, - 'should throw on invalid pseudo elements' - ) - - assert.deepEqual(fromSelector(), h(''), 'should support no selector') - assert.deepEqual(fromSelector(''), h(''), 'should support the empty string') - - assert.deepEqual( - fromSelector('*'), - h(''), - 'should support the universal selector' - ) - - assert.deepEqual( - fromSelector('p i s'), - h('p', h('i', h('s'))), - 'should support the descendant combinator' - ) - - assert.deepEqual( - fromSelector('p > i > s'), - h('p', h('i', h('s'))), - 'should support the child combinator' - ) - - assert.deepEqual( - fromSelector('p i + s'), - h('p', [h('i'), h('s')]), - 'should support the next-sibling combinator' - ) - - assert.deepEqual( - fromSelector('p i ~ s'), - h('p', [h('i'), h('s')]), - 'should support the subsequent-sibling combinator' - ) - - assert.deepEqual(fromSelector('a'), h('a'), 'should support a tag name') - assert.deepEqual(fromSelector('.a'), h('.a'), 'should support a class') - assert.deepEqual( - fromSelector('a.b'), - h('a.b'), - 'should support a tag and a class' - ) - assert.deepEqual(fromSelector('#b'), h('#b'), 'should support an id') - assert.deepEqual( - fromSelector('a#b'), - h('a#b'), - 'should support a tag and an id' - ) - assert.deepEqual( - fromSelector('a#b.c.d'), - h('a#b.c.d'), - 'should support all together' - ) - assert.deepEqual(fromSelector('a#b#c'), h('a#c'), 'should use the last id') - assert.deepEqual(fromSelector('A').tagName, 'a', 'should normalize casing') - - assert.deepEqual( - fromSelector('[a]'), - h('', {a: true}), - 'should support attributes (#1)' - ) - assert.deepEqual( - fromSelector('[a=b]'), - h('', {a: 'b'}), - 'should support attributes (#2)' - ) - - assert.deepEqual( - fromSelector('.a.b[class=c]'), - h('.a.b.c'), - 'should support class and class attributes' - ) - - assert.deepEqual(fromSelector('altGlyph').tagName, 'altglyph', 'space (#1)') - - assert.deepEqual( - fromSelector('altGlyph', 'svg').tagName, - 'altGlyph', - 'space (#2)' - ) - - assert.deepEqual( - fromSelector('altGlyph', {space: 'svg'}).tagName, - 'altGlyph', - 'space (#3)' - ) - - assert.deepEqual( - // @ts-expect-error: fine. - fromSelector('svg altGlyph').children[0].tagName, - 'altGlyph', - 'space (#4)' - ) - - assert.deepEqual( - // @ts-expect-error: fine. - fromSelector('div svg + altGlyph').children[1].tagName, - 'altglyph', - 'space (#5)' - ) - - assert.deepEqual( - fromSelector( - 'p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10] altGlyph' - ), - h('p', [ - s('svg', {viewBox: '0 0 10 10'}, [ - s('circle', {cx: '10', cy: '10', r: '10'}, [s('altGlyph')]) + }, /Cannot handle pseudo element `before`/) + }) + + await t.test('should support no selector', async function () { + assert.deepEqual(fromSelector(), h('')) + }) + + await t.test('should support the empty string', async function () { + assert.deepEqual(fromSelector(''), h('')) + }) + + await t.test('should support the universal selector', async function () { + assert.deepEqual(fromSelector('*'), h('')) + }) + + await t.test('should support the descendant combinator', async function () { + assert.deepEqual(fromSelector('p i s'), h('p', h('i', h('s')))) + }) + + await t.test('should support the child combinator', async function () { + assert.deepEqual(fromSelector('p > i > s'), h('p', h('i', h('s')))) + }) + + await t.test('should support the next-sibling combinator', async function () { + assert.deepEqual(fromSelector('p i + s'), h('p', [h('i'), h('s')])) + }) + + await t.test( + 'should support the subsequent-sibling combinator', + async function () { + assert.deepEqual(fromSelector('p i ~ s'), h('p', [h('i'), h('s')])) + } + ) + + await t.test('should support a tag name', async function () { + assert.deepEqual(fromSelector('a'), h('a')) + }) + + await t.test('should support a class', async function () { + assert.deepEqual(fromSelector('.a'), h('.a')) + }) + + await t.test('should support a tag and a class', async function () { + assert.deepEqual(fromSelector('a.b'), h('a.b')) + }) + + await t.test('should support an id', async function () { + assert.deepEqual(fromSelector('#b'), h('#b')) + }) + + await t.test('should support a tag and an id', async function () { + assert.deepEqual(fromSelector('a#b'), h('a#b')) + }) + + await t.test('should support all together', async function () { + assert.deepEqual(fromSelector('a#b.c.d'), h('a#b.c.d')) + }) + + await t.test('should use the last id', async function () { + assert.deepEqual(fromSelector('a#b#c'), h('a#c')) + }) + + await t.test('should normalize casing', async function () { + assert.equal(fromSelector('A').tagName, 'a') + }) + + await t.test('should support attributes (#1)', async function () { + assert.deepEqual(fromSelector('[a]'), h('', {a: true})) + }) + + await t.test('should support attributes (#2)', async function () { + assert.deepEqual(fromSelector('[a=b]'), h('', {a: 'b'})) + }) + + await t.test('should support class and class attributes', async function () { + assert.deepEqual(fromSelector('.a.b[class=c]'), h('.a.b.c')) + }) + + await t.test('should support space (#1)', async function () { + assert.equal(fromSelector('altGlyph').tagName, 'altglyph') + }) + + await t.test('should support space (#2)', async function () { + assert.equal(fromSelector('altGlyph', 'svg').tagName, 'altGlyph') + }) + + await t.test('should support space (#3)', async function () { + assert.equal(fromSelector('altGlyph', {space: 'svg'}).tagName, 'altGlyph') + }) + + await t.test('should support space (#4)', async function () { + const result = fromSelector('svg altGlyph') + const child = result.children[0] + assert(child.type === 'element') + assert.equal(child.tagName, 'altGlyph') + }) + + await t.test('should support space (#5)', async function () { + const result = fromSelector('div svg + altGlyph') + const child = result.children[1] + assert(child.type === 'element') + assert.equal(child.tagName, 'altglyph') + }) + + await t.test('should support space (#6)', async function () { + assert.deepEqual( + fromSelector( + 'p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10] altGlyph' + ), + h('p', [ + s('svg', {viewBox: '0 0 10 10'}, [ + s('circle', {cx: '10', cy: '10', r: '10'}, [s('altGlyph')]) + ]) ]) - ]), - 'space (#6)' - ) + ) + }) }) From c95de39df7a094c8bfbd8100782c4aba4fb02805 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:13:43 +0200 Subject: [PATCH 06/12] Remove support for passing `space` directly --- lib/index.js | 16 ++++++---------- readme.md | 10 ++++------ test.js | 10 +++------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/index.js b/lib/index.js index 8ead20a..59e32c3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,25 +30,21 @@ import {h, s} from 'hastscript' const cssSelectorParse = createParser({syntax: 'selectors-4'}) -// To do: remove `space` shortcut. +/** @type {Options} */ +const emptyOptions = {} + /** * Create one or more `Element`s from a CSS selector. * * @param {string | null | undefined} [selector=''] * CSS selector (default: `''`). - * @param {Options | Space | null | undefined} [options] + * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {HastElement} * Built tree. */ export function fromSelector(selector, options) { - /** @type {State} */ - const state = { - space: - (options && typeof options === 'object' && options.space) || - (typeof options === 'string' && options) || - 'html' - } + const settings = options || emptyOptions const query = cssSelectorParse(selector || '*') @@ -70,7 +66,7 @@ export function fromSelector(selector, options) { ) } - const result = rule(head, state) + const result = rule(head, {space: settings.space || 'html'}) return result[0] } diff --git a/readme.md b/readme.md index ff87b8a..3aa8623 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ * [Install](#install) * [Use](#use) * [API](#api) - * [`fromSelector(selector?[, options|space])`](#fromselectorselector-optionsspace) + * [`fromSelector(selector?[, options])`](#fromselectorselector-options) * [`Options`](#options) * [`Space`](#space) * [Support](#support) @@ -68,7 +68,7 @@ In browsers with [`esm.sh`][esmsh]: ```js import {fromSelector} from 'hast-util-from-selector' -console.log(fromSelector('p svg[viewbox=0 0 10 10] circle[cx=10][cy=10][r=10]')) +console.log(fromSelector('p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10]')) ``` Yields: @@ -101,7 +101,7 @@ Yields: This package exports the identifier [`fromSelector`][fromselector]. There is no default export. -### `fromSelector(selector?[, options|space])` +### `fromSelector(selector?[, options])` Create one or more [`Element`][element]s from a CSS selector. @@ -111,8 +111,6 @@ Create one or more [`Element`][element]s from a CSS selector. — CSS selector * `options` ([`Options`][options], optional) — configuration -* `space` ([`Space`][space], optional) - — treated as `options.space` ###### Returns @@ -252,7 +250,7 @@ abide by its terms. [hastscript]: https://github.com/syntax-tree/hastscript -[fromselector]: #fromselectorselector-optionsspace +[fromselector]: #fromselectorselector-options [options]: #options diff --git a/test.js b/test.js index 9d4b18e..5d70966 100644 --- a/test.js +++ b/test.js @@ -150,28 +150,24 @@ test('fromSelector', async function (t) { }) await t.test('should support space (#2)', async function () { - assert.equal(fromSelector('altGlyph', 'svg').tagName, 'altGlyph') - }) - - await t.test('should support space (#3)', async function () { assert.equal(fromSelector('altGlyph', {space: 'svg'}).tagName, 'altGlyph') }) - await t.test('should support space (#4)', async function () { + await t.test('should support space (#3)', async function () { const result = fromSelector('svg altGlyph') const child = result.children[0] assert(child.type === 'element') assert.equal(child.tagName, 'altGlyph') }) - await t.test('should support space (#5)', async function () { + await t.test('should support space (#4)', async function () { const result = fromSelector('div svg + altGlyph') const child = result.children[1] assert(child.type === 'element') assert.equal(child.tagName, 'altglyph') }) - await t.test('should support space (#6)', async function () { + await t.test('should support space (#5)', async function () { assert.deepEqual( fromSelector( 'p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10] altGlyph' From 2116a2e3e2c884a99b1856064759c774306a999e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:14:47 +0200 Subject: [PATCH 07/12] Refactor `package.json`, `tsconfig.json` --- package.json | 27 ++++++++++++++------------- tsconfig.json | 10 ++++------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index d562992..4151773 100644 --- a/package.json +++ b/package.json @@ -57,31 +57,32 @@ "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", "test-api": "node --conditions development test.js", - "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", - "test": "npm run format && npm run test-coverage" + "test-coverage": "c8 --100 --reporter lcov npm run test-api", + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, "bracketSpacing": false, "semi": false, - "trailingComma": "none" - }, - "xo": { - "prettier": true, - "rules": { - "unicorn/prefer-at": "off" - } + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "remark-preset-wooorm" ] }, "typeCoverage": { "atLeast": 100, "detail": true, + "ignoreCatch": true, "strict": true + }, + "xo": { + "prettier": true, + "rules": { + "unicorn/prefer-at": "off" + } } } diff --git a/tsconfig.json b/tsconfig.json index 1bc9e99..870d82c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,15 @@ { - "include": ["**/**.js"], - "exclude": ["coverage/", "node_modules/"], "compilerOptions": { "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, "lib": ["es2020"], "module": "node16", - "newLine": "lf", - "skipLibCheck": true, "strict": true, "target": "es2020" - } + }, + "exclude": ["coverage/", "node_modules/"], + "include": ["**/*.js"] } From 75f623ff4102bfdf51d5f1f14ff6b229396af4f9 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:14:59 +0200 Subject: [PATCH 08/12] Refactor `.npmrc` --- .npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 9951b11..3757b30 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -package-lock=false ignore-scripts=true +package-lock=false From 515cdb83912e509dd90245e8200f423b54ec4954 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:16:49 +0200 Subject: [PATCH 09/12] Refactor docs --- readme.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 3aa8623..9a25b4d 100644 --- a/readme.md +++ b/readme.md @@ -43,7 +43,7 @@ and similar to [`hastscript`][hastscript]. ## Install This package is [ESM only][esm]. -In Node.js (version 14.14+ and 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][]: ```sh npm install hast-util-from-selector @@ -98,7 +98,7 @@ Yields: ## API -This package exports the identifier [`fromSelector`][fromselector]. +This package exports the identifier [`fromSelector`][api-from-selector]. There is no default export. ### `fromSelector(selector?[, options])` @@ -107,9 +107,9 @@ Create one or more [`Element`][element]s from a CSS selector. ###### Parameters -* `selector` (`string`, optional) +* `selector` (`string`, default: `''`) — CSS selector -* `options` ([`Options`][options], optional) +* `options` ([`Options`][api-options], optional) — configuration ###### Returns @@ -122,7 +122,7 @@ Configuration (TypeScript type). ###### Fields -* `space` ([`Space`][space], optional) +* `space` ([`Space`][api-space], default: `'html'`) — which space first element in the selector is in. When an `svg` element is created in HTML, the space is automatically switched to SVG @@ -153,14 +153,18 @@ type Space = 'html' | 'svg' ## Types This package is fully typed with [TypeScript][]. -It exports the additional types [`Options`][options] and [`Space`][space]. +It exports the additional types [`Options`][api-options] and +[`Space`][api-space]. ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 14.14+ and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +When we cut a new major release, we drop support for unmaintained versions of +Node. +This means we try to keep the current release line, +`hast-util-from-selector@^2`, compatible with Node.js 12. ## Security @@ -204,9 +208,9 @@ abide by its terms. [downloads]: https://www.npmjs.com/package/hast-util-from-selector -[size-badge]: https://img.shields.io/bundlephobia/minzip/hast-util-from-selector.svg +[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=hast-util-from-selector -[size]: https://bundlephobia.com/result?p=hast-util-from-selector +[size]: https://bundlejs.com/?q=hast-util-from-selector [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg @@ -250,8 +254,8 @@ abide by its terms. [hastscript]: https://github.com/syntax-tree/hastscript -[fromselector]: #fromselectorselector-options +[api-from-selector]: #fromselectorselector-options -[options]: #options +[api-options]: #options -[space]: #space +[api-space]: #space From 230bbc8751a675e5b5ee68c8967b9c7403810446 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:17:12 +0200 Subject: [PATCH 10/12] Change to use `exports` --- package.json | 3 +-- test.js | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4151773..4af46a0 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,7 @@ ], "sideEffects": false, "type": "module", - "main": "index.js", - "types": "index.d.ts", + "exports": "./index.js", "files": [ "lib/", "index.d.ts", diff --git a/test.js b/test.js index 5d70966..d854fa8 100644 --- a/test.js +++ b/test.js @@ -1,13 +1,14 @@ import assert from 'node:assert/strict' import test from 'node:test' import {h, s} from 'hastscript' -import {fromSelector} from './index.js' +import {fromSelector} from 'hast-util-from-selector' test('fromSelector', async function (t) { await t.test('should expose the public api', async function () { - assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ - 'fromSelector' - ]) + assert.deepEqual( + Object.keys(await import('hast-util-from-selector')).sort(), + ['fromSelector'] + ) }) await t.test('should throw w/ invalid selector', async function () { From 4cdcdd319b93df63a8d74fd01c1635820c54e256 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:17:32 +0200 Subject: [PATCH 11/12] Change to require Node.js 16 --- readme.md | 2 +- tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 9a25b4d..329ac7e 100644 --- a/readme.md +++ b/readme.md @@ -164,7 +164,7 @@ versions of Node.js. When we cut a new major release, we drop support for unmaintained versions of Node. This means we try to keep the current release line, -`hast-util-from-selector@^2`, compatible with Node.js 12. +`hast-util-from-selector@^3`, compatible with Node.js 16. ## Security diff --git a/tsconfig.json b/tsconfig.json index 870d82c..82cc749 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,10 @@ "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "lib": ["es2020"], + "lib": ["es2022"], "module": "node16", "strict": true, - "target": "es2020" + "target": "es2022" }, "exclude": ["coverage/", "node_modules/"], "include": ["**/*.js"] From df3455bcb2e33efd8de8873cb665b0162ec62adb Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 4 Aug 2023 15:17:46 +0200 Subject: [PATCH 12/12] 3.0.0 --- package.json | 2 +- readme.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4af46a0..971a6d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hast-util-from-selector", - "version": "2.0.1", + "version": "3.0.0", "description": "hast utility to parse CSS selectors to hast nodes", "license": "MIT", "keywords": [ diff --git a/readme.md b/readme.md index 329ac7e..07e62c4 100644 --- a/readme.md +++ b/readme.md @@ -52,14 +52,14 @@ npm install hast-util-from-selector In Deno with [`esm.sh`][esmsh]: ```js -import {fromSelector} from 'https://esm.sh/hast-util-from-selector@2' +import {fromSelector} from 'https://esm.sh/hast-util-from-selector@3' ``` In browsers with [`esm.sh`][esmsh]: ```html ```