Skip to content

Commit

Permalink
refactor and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
typicode committed Nov 14, 2024
1 parent b3a31b7 commit 2c876d8
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 21 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ div[data-component='section']
If you want both basic links and button-styled links, here’s how you can do:

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

a[data-component='button'] { /* ... */ }
&[data-variant='primary'] { /* ... */ }
Expand All @@ -231,7 +231,7 @@ a[data-component='button'] { /* ... */ }
```

> [!NOTE]
> `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.
> `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.
### How to split my code?

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "rm -rf lib && tsc",
"format": "prettier --write .",
"lint": "eslint",
"test": "postcss test/mist.css",
"test": "node --import tsx --test src/*.test.ts && npm run build && postcss test/mist.css",
"prepublishOnly": "npm run build",
"prepare": "husky"
},
Expand Down
31 changes: 13 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import selectorParser = require('postcss-selector-parser');
import atImport = require("postcss-import")
import path = require('node:path');
const html = require('./html')
const key = require('./key')

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

Object.entries(parsed).forEach(
([key, { tag, attributes, booleanAttributes, properties }]) => {
([key, { tag, rootAttribute, attributes, booleanAttributes, properties }]) => {
const interfaceName = `Mist_${key}`

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

booleanAttributes.forEach((attr) => {
Expand Down Expand Up @@ -82,23 +86,10 @@ function render(parsed: Parsed): string {
return interfaceDefinitions + jsxDeclaration
}

// Turn button[data-component='foo'] into a key that will be used for the interface name
function key(selector: selectorParser.Node): string {
let key = ''
if (selector.type === 'tag') {
key += selector.toString().toLowerCase()
}
const next = selector.next()
if (next?.type === 'attribute') {
const { attribute, value } = next as selectorParser.Attribute
key += `_${attribute}_${value}`
}
return key.replace(/[^a-zA-Z0-9_]/g, '_')
}

function initialParsedValue(): Parsed[keyof Parsed] {
return {
tag: '',
rootAttribute: '',
attributes: {},
booleanAttributes: new Set(),
properties: new Set(),
Expand All @@ -121,6 +112,11 @@ const _mistcss: PluginCreator<{}> = (_opts = {}) => {
if (selector.type === 'tag') {
current = parsed[key(selector)] = initialParsedValue()
current.tag = selector.toString().toLowerCase()
const next = selector.next()
if (next?.type === 'attribute') {
const { attribute, value } = next as selectorParser.Attribute
if (value) current.rootAttribute = attribute
}
}

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

_mistcss.postcss = true

export const mistcss: PluginCreator<{}> = (_opts = {}) => {
const mistcss: PluginCreator<{}> = (_opts = {}) => {
return {
postcssPlugin: 'mistcss',
plugins: [atImport(), _mistcss()]
Expand All @@ -161,5 +157,4 @@ export const mistcss: PluginCreator<{}> = (_opts = {}) => {

mistcss.postcss = true

// Needed to make PostCSS happy
module.exports = mistcss
34 changes: 34 additions & 0 deletions src/key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import selectorParser = require('postcss-selector-parser');
import key = require('./key');

const parser = selectorParser()

test("key", async (t) => {
const arr: [string, string | ErrorConstructor][] = [
['div', 'div'],
['div[data-foo="bar"]', 'div_data_foo_bar'],
['div[data-foo]', 'div_data_foo'],
['Div', 'div'],
['div[data-Foo]', 'div_data_Foo'],
['div[data-foo="1"]', 'div_data_foo_1'],
['div[data-1]', 'div_data_1'],
[' div[ data-foo ] ', 'div_data_foo'],
['div:not([data-component])', 'div'],
['div[data-foo=" bar"]', 'div_data_foo__bar']
]
for (const [input, expected] of arr) {
await t.test(`${input}${expected}`, () => {
const selector = parser.astSync(input, { lossless: false })
if (typeof expected === 'string') {
// @ts-ignore
const actual = key(selector.nodes[0].nodes[0])
assert.equal(actual, expected)
} else {
// @ts-ignore
assert.throws(() => key(selector.nodes[0].nodes[0]))
}
})
}
})
18 changes: 18 additions & 0 deletions src/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import selectorParser = require('postcss-selector-parser');

// Turn button[data-component='foo'] into a key that will be used for the interface name
function key(selector: selectorParser.Node): string {
let key = ''
if (selector.type === 'tag') {
key += selector.toString().toLowerCase()
}
const next = selector.next()
if (next?.type === 'attribute') {
const { attribute, value } = next as selectorParser.Attribute
key += `_${attribute}`
if (value) key += `_${value}`
}
return key.replace(/[^a-zA-Z0-9_]/g, '_')
}

module.exports = key

0 comments on commit 2c876d8

Please sign in to comment.