From ca35b5a937b1b0fc09478ee445a27bb5b1119df1 Mon Sep 17 00:00:00 2001 From: Titus Date: Sun, 9 May 2021 20:49:57 +0200 Subject: [PATCH] Add support for tag name inferral in types Closes GH-5. Closes GH-6. Reviewed-by: Christian Murphy --- extract-legacy.ts | 5 +++ extract.ts | 20 ++++++++++ index.js | 94 ++++++++++++++++++++++++++++------------------- index.test-d.ts | 33 +++++++++++++++++ package.json | 10 ++++- readme.md | 2 +- tsconfig.json | 2 +- 7 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 extract-legacy.ts create mode 100644 extract.ts create mode 100644 index.test-d.ts diff --git a/extract-legacy.ts b/extract-legacy.ts new file mode 100644 index 0000000..82ad4f3 --- /dev/null +++ b/extract-legacy.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export type ExtractTagName = string + +/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/extract.ts b/extract.ts new file mode 100644 index 0000000..43776a9 --- /dev/null +++ b/extract.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export type ExtractTagName< + SimpleSelector extends string, + DefaultTagName extends string +> = SimpleSelector extends `#${infer Rest}` + ? DefaultTagName + : SimpleSelector extends `.${infer Rest}` + ? DefaultTagName + : SimpleSelector extends `${infer TagName}.${infer Rest}` + ? ExtractTagName + : SimpleSelector extends `${infer TagName}#${infer Rest}` + ? TagName + : SimpleSelector extends '' + ? DefaultTagName + : SimpleSelector extends string + ? SimpleSelector + : DefaultTagName + +/* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/index.js b/index.js index be07a33..0a0edd4 100644 --- a/index.js +++ b/index.js @@ -8,46 +8,66 @@ var search = /[#.]/g /** * Create a hast element from a simple CSS selector. * - * @param {string} [selector] - * @param {string} [name='div'] - * @returns {Element} + * @param selector A simple CSS selector. + * Can contain a tag-name (`foo`), classes (`.bar`), and an ID (`#baz`). + * Multiple classes are allowed. + * Uses the last ID if multiple IDs are found. + * @param [defaultTagName='div'] Tag name to use if `selector` does not specify one. */ -export function parseSelector(selector, name = 'div') { - var value = selector || '' - /** @type {Properties} */ - var props = {} - var start = 0 - /** @type {string} */ - var subvalue - /** @type {string} */ - var previous - /** @type {RegExpMatchArray} */ - var match +export const parseSelector = + /** + * @type {( + * (selector?: Selector, defaultTagName?: DefaultTagName) => Element & {tagName: import('./extract.js').ExtractTagName} + * )} + */ + ( + /** + * @param {string} [selector] + * @param {string} [defaultTagName='div'] + * @returns {Element} + */ + function (selector, defaultTagName = 'div') { + var value = selector || '' + /** @type {Properties} */ + var props = {} + var start = 0 + /** @type {string} */ + var subvalue + /** @type {string} */ + var previous + /** @type {RegExpMatchArray} */ + var match - while (start < value.length) { - search.lastIndex = start - match = search.exec(value) - subvalue = value.slice(start, match ? match.index : value.length) + while (start < value.length) { + search.lastIndex = start + match = search.exec(value) + subvalue = value.slice(start, match ? match.index : value.length) - if (subvalue) { - if (!previous) { - name = subvalue - } else if (previous === '#') { - props.id = subvalue - } else if (Array.isArray(props.className)) { - props.className.push(subvalue) - } else { - props.className = [subvalue] - } + if (subvalue) { + if (!previous) { + defaultTagName = subvalue + } else if (previous === '#') { + props.id = subvalue + } else if (Array.isArray(props.className)) { + props.className.push(subvalue) + } else { + props.className = [subvalue] + } - start += subvalue.length - } + start += subvalue.length + } - if (match) { - previous = match[0] - start++ - } - } + if (match) { + previous = match[0] + start++ + } + } - return {type: 'element', tagName: name, properties: props, children: []} -} + return { + type: 'element', + tagName: defaultTagName, + properties: props, + children: [] + } + } + ) diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..a863524 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,33 @@ +import {expectType, expectAssignable} from 'tsd' +import {Element} from 'hast' +import {parseSelector} from './index.js' + +type A = Element & {tagName: 'a'} +type Div = Element & {tagName: 'div'} +type G = Element & {tagName: 'g'} + +// No tag name. +expectType
(parseSelector('')) +expectType
(parseSelector('#id')) +expectType
(parseSelector('.class')) +expectType
(parseSelector('#id.class')) +expectType
(parseSelector('.class#id')) + +// A tag name. +expectType(parseSelector('a')) +expectType(parseSelector('a#id')) +expectType(parseSelector('a.class')) +expectType(parseSelector('a#id.class')) +expectType(parseSelector('a.class#id')) + +// A default tag name +expectType(parseSelector('', 'g')) +expectType(parseSelector('#id', 'g')) +expectType(parseSelector('.class', 'g')) +expectType(parseSelector('#id.class', 'g')) +expectType(parseSelector('.class#id', 'g')) + +// They’re still elements. +expectAssignable(parseSelector('')) +expectAssignable(parseSelector('', 'g')) +expectAssignable(parseSelector('a')) diff --git a/package.json b/package.json index 50fbb36..9c9d49c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,14 @@ "type": "module", "main": "index.js", "types": "index.d.ts", + "typesVersions": { + "<=4.1": { + "extract.d.ts": ["extract-legacy.d.ts"] + } + }, "files": [ + "extract-legacy.d.ts", + "extract.d.ts", "index.d.ts", "index.js" ], @@ -43,13 +50,14 @@ "remark-preset-wooorm": "^8.0.0", "rimraf": "^3.0.0", "tape": "^5.0.0", + "tsd": "^0.14.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "xo": "^0.39.0" }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"*.d.ts\" && tsc && type-coverage", + "build": "rimraf \"*.d.ts\" && tsc && tsd && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test.js", "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", diff --git a/readme.md b/readme.md index 174c65d..a1fcd33 100644 --- a/readme.md +++ b/readme.md @@ -50,7 +50,7 @@ Create an [*element*][element] [*node*][node] from a simple CSS selector. ###### `selector` -`string`, optional — Can contain a tag-name (`foo`), classes (`.bar`), +`string`, optional — Can contain a tag name (`foo`), classes (`.bar`), and an ID (`#baz`). Multiple classes are allowed. Uses the last ID if multiple IDs are found. diff --git a/tsconfig.json b/tsconfig.json index be08abe..1a8d675 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["*.js"], + "include": ["index.js", "test.js", "extract.ts", "extract-legacy.ts"], "compilerOptions": { "target": "ES2020", "lib": ["ES2020"],