Skip to content

Commit 2c876d8

Browse files
committed
refactor and add tests
1 parent b3a31b7 commit 2c876d8

File tree

5 files changed

+68
-21
lines changed

5 files changed

+68
-21
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ div[data-component='section']
214214
If you want both basic links and button-styled links, here’s how you can do:
215215

216216
```css
217-
a { /* ... */ }
217+
a:not([data-component]) { /* ... */ }
218218

219219
a[data-component='button'] { /* ... */ }
220220
&[data-variant='primary'] { /* ... */ }
@@ -231,7 +231,7 @@ a[data-component='button'] { /* ... */ }
231231
```
232232

233233
> [!NOTE]
234-
> `data-component` is just a naming convention. Feel free to use any attribute, like `data-style='button'` or `data-button`. It’s simply a way to differentiate between components using the same tag.
234+
> `data-component` is just a naming convention. Feel free to use any attribute, like `data-kind='button'` or just `data-c`. It’s simply a way to differentiate between components using the same tag.
235235
236236
### How to split my code?
237237

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build": "rm -rf lib && tsc",
88
"format": "prettier --write .",
99
"lint": "eslint",
10-
"test": "postcss test/mist.css",
10+
"test": "node --import tsx --test src/*.test.ts && npm run build && postcss test/mist.css",
1111
"prepublishOnly": "npm run build",
1212
"prepare": "husky"
1313
},

src/index.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import selectorParser = require('postcss-selector-parser');
44
import atImport = require("postcss-import")
55
import path = require('node:path');
66
const html = require('./html')
7+
const key = require('./key')
78

89
declare module 'postcss-selector-parser' {
910
// For some reasons these aren't avaiblable in this module types
@@ -18,6 +19,7 @@ type Parsed = Record<
1819
string,
1920
{
2021
tag: string
22+
rootAttribute: string
2123
attributes: Record<string, Set<string>>
2224
booleanAttributes: Set<string>
2325
properties: Set<string>
@@ -29,7 +31,7 @@ function render(parsed: Parsed): string {
2931
const jsxElements: Record<string, string[]> = {}
3032

3133
Object.entries(parsed).forEach(
32-
([key, { tag, attributes, booleanAttributes, properties }]) => {
34+
([key, { tag, rootAttribute, attributes, booleanAttributes, properties }]) => {
3335
const interfaceName = `Mist_${key}`
3436

3537
const attributeEntries = Object.entries(attributes)
@@ -45,7 +47,9 @@ function render(parsed: Parsed): string {
4547
const valueType = Array.from(values)
4648
.map((v) => `'${v}'`)
4749
.join(' | ')
48-
interfaceDefinition += ` '${attr}'?: ${valueType}\n`
50+
// Root attribute is used to narrow type and therefore is the only attribute
51+
// that shouldn't be optional (i.e. attr: ... and not attr?: ...)
52+
interfaceDefinition += ` '${attr}'${rootAttribute === attr ? '' : '?'}: ${valueType}\n`
4953
})
5054

5155
booleanAttributes.forEach((attr) => {
@@ -82,23 +86,10 @@ function render(parsed: Parsed): string {
8286
return interfaceDefinitions + jsxDeclaration
8387
}
8488

85-
// Turn button[data-component='foo'] into a key that will be used for the interface name
86-
function key(selector: selectorParser.Node): string {
87-
let key = ''
88-
if (selector.type === 'tag') {
89-
key += selector.toString().toLowerCase()
90-
}
91-
const next = selector.next()
92-
if (next?.type === 'attribute') {
93-
const { attribute, value } = next as selectorParser.Attribute
94-
key += `_${attribute}_${value}`
95-
}
96-
return key.replace(/[^a-zA-Z0-9_]/g, '_')
97-
}
98-
9989
function initialParsedValue(): Parsed[keyof Parsed] {
10090
return {
10191
tag: '',
92+
rootAttribute: '',
10293
attributes: {},
10394
booleanAttributes: new Set(),
10495
properties: new Set(),
@@ -121,6 +112,11 @@ const _mistcss: PluginCreator<{}> = (_opts = {}) => {
121112
if (selector.type === 'tag') {
122113
current = parsed[key(selector)] = initialParsedValue()
123114
current.tag = selector.toString().toLowerCase()
115+
const next = selector.next()
116+
if (next?.type === 'attribute') {
117+
const { attribute, value } = next as selectorParser.Attribute
118+
if (value) current.rootAttribute = attribute
119+
}
124120
}
125121

126122
if (selector.type === 'attribute') {
@@ -152,7 +148,7 @@ const _mistcss: PluginCreator<{}> = (_opts = {}) => {
152148

153149
_mistcss.postcss = true
154150

155-
export const mistcss: PluginCreator<{}> = (_opts = {}) => {
151+
const mistcss: PluginCreator<{}> = (_opts = {}) => {
156152
return {
157153
postcssPlugin: 'mistcss',
158154
plugins: [atImport(), _mistcss()]
@@ -161,5 +157,4 @@ export const mistcss: PluginCreator<{}> = (_opts = {}) => {
161157

162158
mistcss.postcss = true
163159

164-
// Needed to make PostCSS happy
165160
module.exports = mistcss

src/key.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
import selectorParser = require('postcss-selector-parser');
4+
import key = require('./key');
5+
6+
const parser = selectorParser()
7+
8+
test("key", async (t) => {
9+
const arr: [string, string | ErrorConstructor][] = [
10+
['div', 'div'],
11+
['div[data-foo="bar"]', 'div_data_foo_bar'],
12+
['div[data-foo]', 'div_data_foo'],
13+
['Div', 'div'],
14+
['div[data-Foo]', 'div_data_Foo'],
15+
['div[data-foo="1"]', 'div_data_foo_1'],
16+
['div[data-1]', 'div_data_1'],
17+
[' div[ data-foo ] ', 'div_data_foo'],
18+
['div:not([data-component])', 'div'],
19+
['div[data-foo=" bar"]', 'div_data_foo__bar']
20+
]
21+
for (const [input, expected] of arr) {
22+
await t.test(`${input}${expected}`, () => {
23+
const selector = parser.astSync(input, { lossless: false })
24+
if (typeof expected === 'string') {
25+
// @ts-ignore
26+
const actual = key(selector.nodes[0].nodes[0])
27+
assert.equal(actual, expected)
28+
} else {
29+
// @ts-ignore
30+
assert.throws(() => key(selector.nodes[0].nodes[0]))
31+
}
32+
})
33+
}
34+
})

src/key.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import selectorParser = require('postcss-selector-parser');
2+
3+
// Turn button[data-component='foo'] into a key that will be used for the interface name
4+
function key(selector: selectorParser.Node): string {
5+
let key = ''
6+
if (selector.type === 'tag') {
7+
key += selector.toString().toLowerCase()
8+
}
9+
const next = selector.next()
10+
if (next?.type === 'attribute') {
11+
const { attribute, value } = next as selectorParser.Attribute
12+
key += `_${attribute}`
13+
if (value) key += `_${value}`
14+
}
15+
return key.replace(/[^a-zA-Z0-9_]/g, '_')
16+
}
17+
18+
module.exports = key

0 commit comments

Comments
 (0)