Skip to content

Commit

Permalink
Add support for tag name inferral in types
Browse files Browse the repository at this point in the history
Closes GH-5.
Closes GH-6.

Reviewed-by: Christian Murphy <christian.murphy.42@gmail.com>
  • Loading branch information
wooorm authored May 9, 2021
1 parent 8377fe1 commit ca35b5a
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 40 deletions.
5 changes: 5 additions & 0 deletions extract-legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

export type ExtractTagName<X, Y> = string

/* eslint-enable @typescript-eslint/no-unused-vars */
20 changes: 20 additions & 0 deletions extract.ts
Original file line number Diff line number Diff line change
@@ -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<TagName, DefaultTagName>
: SimpleSelector extends `${infer TagName}#${infer Rest}`
? TagName
: SimpleSelector extends ''
? DefaultTagName
: SimpleSelector extends string
? SimpleSelector
: DefaultTagName

/* eslint-enable @typescript-eslint/no-unused-vars */
94 changes: 57 additions & 37 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends string, DefaultTagName extends string = 'div'>(selector?: Selector, defaultTagName?: DefaultTagName) => Element & {tagName: import('./extract.js').ExtractTagName<Selector, DefaultTagName>}
* )}
*/
(
/**
* @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: []
}
}
)
33 changes: 33 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<Div>(parseSelector(''))
expectType<Div>(parseSelector('#id'))
expectType<Div>(parseSelector('.class'))
expectType<Div>(parseSelector('#id.class'))
expectType<Div>(parseSelector('.class#id'))

// A tag name.
expectType<A>(parseSelector('a'))
expectType<A>(parseSelector('a#id'))
expectType<A>(parseSelector('a.class'))
expectType<A>(parseSelector('a#id.class'))
expectType<A>(parseSelector('a.class#id'))

// A default tag name
expectType<G>(parseSelector('', 'g'))
expectType<G>(parseSelector('#id', 'g'))
expectType<G>(parseSelector('.class', 'g'))
expectType<G>(parseSelector('#id.class', 'g'))
expectType<G>(parseSelector('.class#id', 'g'))

// They’re still elements.
expectAssignable<Element>(parseSelector(''))
expectAssignable<Element>(parseSelector('', 'g'))
expectAssignable<Element>(parseSelector('a'))
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["*.js"],
"include": ["index.js", "test.js", "extract.ts", "extract-legacy.ts"],
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
Expand Down

0 comments on commit ca35b5a

Please sign in to comment.