Skip to content

Commit

Permalink
core change: Refactors
Browse files Browse the repository at this point in the history
- Collapses `reducer()` into `parser()` function.
- Improves `reducer()` function, does not need to handle Array anymore.

refactor:
- Refactor lite `parser()` function.

chore:
- Test also lite `replacer()` and `twg()` function.
- Add more test cases.
  • Loading branch information
hoangnhan2ka3 committed Aug 24, 2024
1 parent 6e1c9b3 commit 1dfddbc
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 92 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ permissions:
jobs:
create-release:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
steps:
- uses: taiki-e/checkout-action@v1
- uses: taiki-e/create-gh-release-action@v1
with:
changelog: CHANGELOG.md
draft: true
title: $tag
branch: 'main|v[0-9]+'
token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
runner: [ubuntu-latest, windows-latest]

runs-on: ${{ matrix.runner }}
timeout-minutes: 15
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
Expand Down
37 changes: 35 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
# Changelog

[`twg`](https://github.com/hoangnhan2ka3/twg) adheres to [Semantic Versioning](http://semver.org/).
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Undocumented APIs should be considered internal and may change without warning.

## [Unreleased]

### Core change

- Use `@babel` AST to parse all conditional classes or conditional objects.
- **Pros:**
- More accurate, more trust in processing.
- Reduce several complex regex use to parse condition.
- Lighter bundle.
- **Cons:**
- Currently work on (.jsx, .tsx file) only.
- A bit slower, especially on the first time, when nothing is cached.
- 4 more dependencies.

## [1.2.3] - 2024-08-24

### Core change

- Collapses `reducer()` into `parser()` function.
- Improves `reducer()` function, does not need to handle Array anymore.

### Refactor

- Refactor lite `parser()` function.

### Chore

- Test also lite `replacer()` and `twg()` function.
- Add more test cases.

## [1.2.2] - 2024-08-24

### Chore
Expand All @@ -19,7 +52,7 @@ Undocumented APIs should be considered internal and may change without warning.

## [1.2.0] - 2024-08-23

### Core changes
### Core change

- Change the behavior of `extractor()` to scan also callee function and outer object(s) inside it.
- Doesn't need regex to match `callee` function anymore.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "twg",
"version": "1.2.2",
"version": "1.2.3",
"description": "A utility function for grouping TailwindCSS variants.",
"author": "Nguyễn Hoàng Nhân <[email protected]> (https://github.com/hoangnhan2ka3)",
"homepage": "https://github.com/hoangnhan2ka3/twg",
Expand Down
2 changes: 1 addition & 1 deletion src/lite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function toVal(mix: ClassValue): string {
}
}
} else {
str += parser(mix)
str += parser()(mix)
}
}
return str
Expand Down
52 changes: 23 additions & 29 deletions src/lite/lite-processor/parser.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import { type ClassValue } from "src/lite"

function reducer(args: ClassValue[]) {
return (
args.reduce<string[]>((acc: string[], cur: ClassValue) => {
if (cur === undefined || cur === null || cur === false) return acc
if (Array.isArray(cur)) {
acc.push(...cur.filter(Boolean).map(String))
} else if (typeof cur === "object") {
export function parser() {
function reducer(args: ClassValue[]): string[] {
return args.reduce<string[]>((acc, cur) => {
if (cur && typeof cur === "object") {
Object.entries(cur).forEach(([key, values]) => {
const func = parser[key] as (...args: ClassValue[]) => string
if (Array.isArray(values)) {
values.flat(Infinity).forEach((value: ClassValue) => {
acc.push(func(value))
})
} else {
acc.push(func(values as ClassValue))
}
(Array.isArray(values) ? values.flat(Infinity) : [values]).forEach(
value => acc.push(
(parser()[key] as (...args: ClassValue[]) => string)(value as ClassValue)
)
)
})
} else {
acc.push(...String(cur).split(" "))
}
return acc
}, [])
).flat()
}

export const parser = new Proxy((...args: ClassValue[]) => {
return reducer(args).join(" ")
}, {
get: function (obj, key: string) {
return key ? (
...args: ClassValue[]
) => reducer(args).filter(values => values.trim() !== "").map((values) => (
`${key}:${values}`.trim()
)) : obj
}, []).flat()
}
}) as Record<string, (...args: ClassValue[]) => string> & ((...args: ClassValue[]) => string)

return new Proxy((...args: ClassValue[]) => {
return reducer(args).join(" ")
}, {
get: function (obj, key: string) {
return key ? (
...args: ClassValue[]
) => reducer(args).filter(values => values.trim() !== "").map((values) => (
`${key}:${values}`.trim()
)) : obj
}
}) as Record<string, (...args: ClassValue[]) => string> & ((...args: ClassValue[]) => string)
}
6 changes: 3 additions & 3 deletions src/lite/replacer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ const replaceTernaryClasses = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?\?\s*(['"`]
const replaceAndOrConsequent = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?(?:&&|\|\||\?\?|\?)\s*/g
const replaceAlternative = /\}\s*:\s*\{/gs

export default function replacer({ callee = "twg" }: ReplacerLiteOption = {}) {
export function replacer({ callee = "twg" }: ReplacerLiteOption = {}) {
return (content: string) => {
if (callee.length === 0) callee = "twg"

try {
extractor(content, callee).forEach(largestObject => {
const filteredObject = largestObject
const filteredObject = (/['"`]/).test(largestObject)
? largestObject
.replace(replaceTernaryClasses, '"$2 $3"')
.replace(replaceAndOrConsequent, "")
.replace(replaceAlternative, "}, {")
: ""

try {
const parsedObject = parser(
const parsedObject = parser()(
...new Function(`return [${filteredObject}]`)() as ClassValue[]
)
content = content.replace(largestObject, `"${parsedObject}"`)
Expand Down
63 changes: 26 additions & 37 deletions src/processor/parser.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,51 @@
import { type ClassValue, type TWGOptions } from "src/index"

/**
* Focusing on handling arrays and objects, looping them until all are flattened.
* @param args The inputs class values
* @param separator The separator used to join the classes
* @returns string[]
* @author `easy-tailwind` [Noriller] see <[reference](https://github.com/Noriller/easy-tailwind/blob/master/src/index.ts#L65C1-L89C2)>
*/
function reducer(args: ClassValue[], options?: TWGOptions) {
return (
args.reduce<string[]>((acc: string[], cur: ClassValue) => {
if (cur === undefined || cur === null || cur === false) return acc
if (Array.isArray(cur)) {
acc.push(...cur.filter(Boolean).map(String))
} else if (typeof cur === "object") {
Object.entries(cur).forEach(([key, values]) => {
const func = parser(options)[key] as (...args: ClassValue[]) => string
if (Array.isArray(values)) {
values.flat(Infinity).forEach((value: ClassValue) => {
acc.push(func(value))
})
} else {
acc.push(func(values as ClassValue))
}
})
} else {
acc.push(...String(cur).split(" "))
}
return acc
}, [])
).flat()
}

/**
* Transforms the inputs. Map key to each values inside the Object zones.
* @param options see [docs](https://github.com/hoangnhan2ka3/twg?tab=readme-ov-file#twg-options).
* @param args The inputs class values
* @returns string
* @author `easy-tailwind` [Noriller] see <[reference](https://github.com/Noriller/easy-tailwind/blob/master/src/index.ts#L57C1-L63C4)>
*/
export function parser(options?: TWGOptions) {
const divider = (options?.separator !== undefined)
? typeof options.separator === "string"
? options.separator
: ""
: ":"

/**
* Focusing on handling arrays and objects, looping them until all are flattened.
* @param args The inputs class values
* @param separator The separator used to join the classes
* @returns string[]
*/
function reducer(args: ClassValue[]): string[] {
return args.reduce<string[]>((acc, cur) => {
if (cur && typeof cur === "object") {
Object.entries(cur).forEach(([key, values]) => {
(Array.isArray(values) ? values.flat(Infinity) : [values]).forEach(
value => acc.push(
(parser(options)[key] as (...args: ClassValue[]) => string)(value as ClassValue)
)
)
})
} else {
acc.push(...String(cur).split(" "))
}
return acc
}, []).flat()
}

return new Proxy((...args: ClassValue[]) => {
return reducer(args, options).join(" ")
return reducer(args).join(" ")
}, {
get: function (obj, key: string) {
return key ? (
...args: ClassValue[]
// filter out empty strings/spaces
) => reducer(args, options).filter(values => values.trim() !== "").map((values) => (
) => reducer(args).filter(values => values.trim() !== "").map((values) => (
`${key}${divider}${values}`.trim()
)) : obj
}
}) as Record<string, (...args: ClassValue[]) => string> & ((...args: ClassValue[]) => string)
}

export default parser
2 changes: 1 addition & 1 deletion src/replacer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface ReplacerOptions {

const replaceTernaryClasses = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?\?\s*(['"`])(.*?)\1\s*:\s*\1(.*?)\1/gs // cond (=== prop) ? $2 : $3
const replaceAndOrConsequent = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?(?:&&|\|\||\?\?|\?)\s*/g // cond (=== prop) &&, ||, ??, ?
const replaceAlternative = /\}\s*:\s*\{/gs // } : {
const replaceAlternative = /\}\s*:\s*\{/g // } : {

/**
* Transforms the content before Tailwind scans/extracting its classes.
Expand Down
61 changes: 53 additions & 8 deletions tests/dev/extractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,90 @@ describe("extractOuterObjects()", () => {
it.each([
{
contents: `
twg("multiple classes")
<div className={twg("multiple classes"))} />
`,
expected: []
},
{
contents: `
twg({
<div className={twg({
mod1: ["class", "other classes"],
mod2: ["multiple classes"]
})
})} />
`,
expected: [`{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`]
},
{
contents: `
twg(
<div className={twg(
"multiple classes",
{
mod1: ["class", "other classes"],
mod2: ["multiple classes"]
}
)
)} />
`,
expected: [`{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`]
},
{
contents: `
twg(
<div className={twg(
"multiple classes",
{
mod1: ["class", "other classes"],
mod2: ["class", { "additional-mod": "other classes" }]
}
)
)} />
`,
expected: [`{ mod1: ["class", "other classes"], mod2: ["class", { "additional-mod": "other classes" }] }`]
},
{
contents: `
<div className={twg(
"multiple classes",
{
var1: \`multiple
classes\`,
var2: [
"multiple classes", {
var3: \`other
class\`
}]
},
className
)} />
`,
expected: ["{ var1: `multiple classes`, var2: [ \"multiple classes\", { var3: `other class` }] }"]
}
])('"$expected"', ({ contents, expected }) => {
expect(extractor(contents.replace(/\s\s+/g, " "))).toStrictEqual(expected)
})
})

describe("Multiple callee functions:", () => {
it.each([
{
contents: `
<div className={cn({
mod1: ["class", "other classes"],
mod2: ["multiple classes"]
})} />
Hello World
<div className={cn(
"multiple classes",
{
mod3: ["class", "other multiple classes"],
mod4: ["multiple classes"]
}
)} />
`,
expected: [
`{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`,
`{ mod3: ["class", "other multiple classes"], mod4: ["multiple classes"] }`
]
}
])('"$expected"', ({ contents, expected }) => {
expect(extractor(contents.replace(/\s\s+/g, " "), "twg")).toStrictEqual(expected)
expect(extractor(contents.replace(/\s\s+/g, " "), "cn")).toStrictEqual(expected)
})
})
})
Loading

0 comments on commit 1dfddbc

Please sign in to comment.