Skip to content

Commit ca35b5a

Browse files
authored
Add support for tag name inferral in types
Closes GH-5. Closes GH-6. Reviewed-by: Christian Murphy <[email protected]>
1 parent 8377fe1 commit ca35b5a

File tree

7 files changed

+126
-40
lines changed

7 files changed

+126
-40
lines changed

Diff for: extract-legacy.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
3+
export type ExtractTagName<X, Y> = string
4+
5+
/* eslint-enable @typescript-eslint/no-unused-vars */

Diff for: extract.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
3+
export type ExtractTagName<
4+
SimpleSelector extends string,
5+
DefaultTagName extends string
6+
> = SimpleSelector extends `#${infer Rest}`
7+
? DefaultTagName
8+
: SimpleSelector extends `.${infer Rest}`
9+
? DefaultTagName
10+
: SimpleSelector extends `${infer TagName}.${infer Rest}`
11+
? ExtractTagName<TagName, DefaultTagName>
12+
: SimpleSelector extends `${infer TagName}#${infer Rest}`
13+
? TagName
14+
: SimpleSelector extends ''
15+
? DefaultTagName
16+
: SimpleSelector extends string
17+
? SimpleSelector
18+
: DefaultTagName
19+
20+
/* eslint-enable @typescript-eslint/no-unused-vars */

Diff for: index.js

+57-37
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,66 @@ var search = /[#.]/g
88
/**
99
* Create a hast element from a simple CSS selector.
1010
*
11-
* @param {string} [selector]
12-
* @param {string} [name='div']
13-
* @returns {Element}
11+
* @param selector A simple CSS selector.
12+
* Can contain a tag-name (`foo`), classes (`.bar`), and an ID (`#baz`).
13+
* Multiple classes are allowed.
14+
* Uses the last ID if multiple IDs are found.
15+
* @param [defaultTagName='div'] Tag name to use if `selector` does not specify one.
1416
*/
15-
export function parseSelector(selector, name = 'div') {
16-
var value = selector || ''
17-
/** @type {Properties} */
18-
var props = {}
19-
var start = 0
20-
/** @type {string} */
21-
var subvalue
22-
/** @type {string} */
23-
var previous
24-
/** @type {RegExpMatchArray} */
25-
var match
17+
export const parseSelector =
18+
/**
19+
* @type {(
20+
* <Selector extends string, DefaultTagName extends string = 'div'>(selector?: Selector, defaultTagName?: DefaultTagName) => Element & {tagName: import('./extract.js').ExtractTagName<Selector, DefaultTagName>}
21+
* )}
22+
*/
23+
(
24+
/**
25+
* @param {string} [selector]
26+
* @param {string} [defaultTagName='div']
27+
* @returns {Element}
28+
*/
29+
function (selector, defaultTagName = 'div') {
30+
var value = selector || ''
31+
/** @type {Properties} */
32+
var props = {}
33+
var start = 0
34+
/** @type {string} */
35+
var subvalue
36+
/** @type {string} */
37+
var previous
38+
/** @type {RegExpMatchArray} */
39+
var match
2640

27-
while (start < value.length) {
28-
search.lastIndex = start
29-
match = search.exec(value)
30-
subvalue = value.slice(start, match ? match.index : value.length)
41+
while (start < value.length) {
42+
search.lastIndex = start
43+
match = search.exec(value)
44+
subvalue = value.slice(start, match ? match.index : value.length)
3145

32-
if (subvalue) {
33-
if (!previous) {
34-
name = subvalue
35-
} else if (previous === '#') {
36-
props.id = subvalue
37-
} else if (Array.isArray(props.className)) {
38-
props.className.push(subvalue)
39-
} else {
40-
props.className = [subvalue]
41-
}
46+
if (subvalue) {
47+
if (!previous) {
48+
defaultTagName = subvalue
49+
} else if (previous === '#') {
50+
props.id = subvalue
51+
} else if (Array.isArray(props.className)) {
52+
props.className.push(subvalue)
53+
} else {
54+
props.className = [subvalue]
55+
}
4256

43-
start += subvalue.length
44-
}
57+
start += subvalue.length
58+
}
4559

46-
if (match) {
47-
previous = match[0]
48-
start++
49-
}
50-
}
60+
if (match) {
61+
previous = match[0]
62+
start++
63+
}
64+
}
5165

52-
return {type: 'element', tagName: name, properties: props, children: []}
53-
}
66+
return {
67+
type: 'element',
68+
tagName: defaultTagName,
69+
properties: props,
70+
children: []
71+
}
72+
}
73+
)

Diff for: index.test-d.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {expectType, expectAssignable} from 'tsd'
2+
import {Element} from 'hast'
3+
import {parseSelector} from './index.js'
4+
5+
type A = Element & {tagName: 'a'}
6+
type Div = Element & {tagName: 'div'}
7+
type G = Element & {tagName: 'g'}
8+
9+
// No tag name.
10+
expectType<Div>(parseSelector(''))
11+
expectType<Div>(parseSelector('#id'))
12+
expectType<Div>(parseSelector('.class'))
13+
expectType<Div>(parseSelector('#id.class'))
14+
expectType<Div>(parseSelector('.class#id'))
15+
16+
// A tag name.
17+
expectType<A>(parseSelector('a'))
18+
expectType<A>(parseSelector('a#id'))
19+
expectType<A>(parseSelector('a.class'))
20+
expectType<A>(parseSelector('a#id.class'))
21+
expectType<A>(parseSelector('a.class#id'))
22+
23+
// A default tag name
24+
expectType<G>(parseSelector('', 'g'))
25+
expectType<G>(parseSelector('#id', 'g'))
26+
expectType<G>(parseSelector('.class', 'g'))
27+
expectType<G>(parseSelector('#id.class', 'g'))
28+
expectType<G>(parseSelector('.class#id', 'g'))
29+
30+
// They’re still elements.
31+
expectAssignable<Element>(parseSelector(''))
32+
expectAssignable<Element>(parseSelector('', 'g'))
33+
expectAssignable<Element>(parseSelector('a'))

Diff for: package.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@
2828
"type": "module",
2929
"main": "index.js",
3030
"types": "index.d.ts",
31+
"typesVersions": {
32+
"<=4.1": {
33+
"extract.d.ts": ["extract-legacy.d.ts"]
34+
}
35+
},
3136
"files": [
37+
"extract-legacy.d.ts",
38+
"extract.d.ts",
3239
"index.d.ts",
3340
"index.js"
3441
],
@@ -43,13 +50,14 @@
4350
"remark-preset-wooorm": "^8.0.0",
4451
"rimraf": "^3.0.0",
4552
"tape": "^5.0.0",
53+
"tsd": "^0.14.0",
4654
"type-coverage": "^2.0.0",
4755
"typescript": "^4.0.0",
4856
"xo": "^0.39.0"
4957
},
5058
"scripts": {
5159
"prepack": "npm run build && npm run format",
52-
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
60+
"build": "rimraf \"*.d.ts\" && tsc && tsd && type-coverage",
5361
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
5462
"test-api": "node test.js",
5563
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js",

Diff for: readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Create an [*element*][element] [*node*][node] from a simple CSS selector.
5050

5151
###### `selector`
5252

53-
`string`, optional — Can contain a tag-name (`foo`), classes (`.bar`),
53+
`string`, optional — Can contain a tag name (`foo`), classes (`.bar`),
5454
and an ID (`#baz`).
5555
Multiple classes are allowed.
5656
Uses the last ID if multiple IDs are found.

Diff for: tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"include": ["*.js"],
2+
"include": ["index.js", "test.js", "extract.ts", "extract-legacy.ts"],
33
"compilerOptions": {
44
"target": "ES2020",
55
"lib": ["ES2020"],

0 commit comments

Comments
 (0)