Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support arbitrary value #82

Merged
merged 7 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-steaks-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"scalawind": patch
---

Support arbitrary values
5 changes: 5 additions & 0 deletions .changeset/wicked-seahorses-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"scalawind": minor
---

support arbitrary values
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ button(cls := sw(tw.important(tw.text_black).hover(tw.important(tw.text_blue_700
<button class="!text-black hover:!text-blue-700">Click me</button>
```

## Arbitrary value

We have support for [arbitrary values](https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values) with quite similar signature, instead of wrapping your arbitrary value in square brackets, you now use function call. For example:

```scala
val styles: String = tw.bg_("#bada55").text_("22px")

// ↓ ↓ ↓ ↓ ↓ ↓

val styles: String = "bg-[#bada55] text-[22px]"
```

Because this is quite "arbitrary" thing that you might not want your team to break your design system, you need to explicitly enable this feature via `--arbitrary-values` (or `-av` alias) flag.

## Arbitrary variants

We have support for [arbitrary variants](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-variants) feature.
Expand Down Expand Up @@ -429,6 +443,15 @@ As you can see, the whole scalawind thing includes two parts:
- The `case class Tailwind`, the `object tw` and the `extension (tailwind: Tailwind):` is just there for the Fluent Syntax.
- The `sw` and `swImpl` is the macro that will compile all those fluent thing into a string

## Troubleshoot

### Method too large

Unlike Tailwind which generate code based on your actual usage of utility classes in your source code, Scalawind needs to generate all the utility classes in advance so that we can benefit from type safety.

This pre-generation behavior might generate a huge `extractClassNames` method used in our macro, which traverse over the fluent method calls to extract utilities classes.

In order to solve this problem, we suggest that you follow the [Reducing Generated Code Size](#reducing-generated-code-size) section.

## Acknowledgement

Expand Down
8 changes: 7 additions & 1 deletion packages/scalawind/src/commands/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const initOptionsSchema = z.object({
cwd: z.string(),
output: z.string(),
packageName: z.string(),
previewCompliedResult: z.boolean()
previewCompliedResult: z.boolean(),
arbitraryValues: z.boolean()
})

export const generate = new Command()
Expand All @@ -34,6 +35,11 @@ export const generate = new Command()
"enable show preview compiled result",
false
)
.option(
"-av, --arbitrary-values",
"enable support for arbitrary values",
false
)
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
Expand Down
28 changes: 24 additions & 4 deletions packages/scalawind/src/generate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const scalawindTemplate = fs.readFileSync(path.join(__dirname, "./templates/scal

const template = Handlebars.compile(scalawindTemplate);

export function generateContent(userConfig, packageName = "scalawind", previewCompliedResult = false) {
export function generateContent(userConfig, packageName = "scalawind", previewCompliedResult = false, arbitraryValues = false) {
const resolvedConfig = resolveConfig(userConfig);
const ctx = createContext(resolvedConfig);

Expand Down Expand Up @@ -70,6 +70,20 @@ export function generateContent(userConfig, packageName = "scalawind", previewCo
return { prop: fmtToScalawind(s), raw: s, doc: createDoc(css) };
})

const candidates = [...candidateRuleMap.entries()];
const arbitrary = [];
if(arbitraryValues) {
for (const [name] of candidates) {
const ident = fmtToScalawind(name) + '_';
// edge case, we don't want the *_ method
if(ident === "*_") {
continue
}

arbitrary.push({ methodName: ident, value: `${name}-`})
}
}

const modifiers = [...variantMap.keys()]
// Remove * from the list of modifiers to avoid syntax error
.filter((s) => s !== '*')
Expand All @@ -80,7 +94,13 @@ export function generateContent(userConfig, packageName = "scalawind", previewCo
return ({ name: mod, value: mod})
})

const generatedScalawind = template({ package: packageName, modifiers, standard, previewCompliedResult })
const generatedScalawind = template({
packageName,
modifiers,
standard,
previewCompliedResult,
arbitrary,
})

return generatedScalawind
}
Expand Down Expand Up @@ -147,7 +167,7 @@ export function writeToDisk(path, content) {
}

export default function generate(userConfig, options) {
const { packageName, output, previewCompliedResult } = options
const content = generateContent(userConfig, packageName, previewCompliedResult)
const { packageName, output, previewCompliedResult, arbitraryValues } = options
const content = generateContent(userConfig, packageName, previewCompliedResult, arbitraryValues)
writeToDisk(output, content)
}
17 changes: 13 additions & 4 deletions packages/scalawind/src/generate/templates/scalawind.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package {{package}}
package {{packageName}}

import scala.quoted.*
import scala.annotation.unused
Expand All @@ -10,19 +10,20 @@ case class Tailwind() {
def important(@unused styles: Tailwind): Tailwind = Tailwind()
def i(@unused styles: Tailwind): Tailwind = Tailwind()
def raw(@unused classString: String): Tailwind = Tailwind()
def variant(selector: String, styles: Tailwind): Tailwind = Tailwind()
def opacity(value: Int): Tailwind = Tailwind()
def o(value: Int): Tailwind = Tailwind()
def variant(selector: String, styles: Tailwind): Tailwind = Tailwind()
}

object tw {
def apply(): Tailwind = Tailwind()

{{#each standard}}
{{this.doc}}
def {{this.prop}}: Tailwind = Tailwind()
{{/each}}

{{#each arbitrary}}
def {{this.methodName}}(value: String): Tailwind = Tailwind()
{{/each}}
{{#each modifiers}}
def {{this.name}}(@unused styles: Tailwind): Tailwind = Tailwind()
{{/each}}
Expand All @@ -37,6 +38,10 @@ extension (tailwind: Tailwind)
{{this.doc}}
def {{this.prop}}: Tailwind = Tailwind()
{{/each}}
{{#each arbitrary}}
def {{this.methodName}}(value: String): Tailwind = Tailwind()
{{/each}}


inline def sw(inline tailwind: Tailwind): String =
${ swImpl('tailwind) }
Expand All @@ -50,6 +55,10 @@ def swImpl(twStyleExpr: Expr[Tailwind])(using Quotes): Expr[String] = {
val classes = extractClassNames(styles).map(clx => s"{{this.value}}:$clx")
extractClassNames(inner) ++ classes
{{/each}}
{{#each arbitrary}}
case Apply(Select(inner, "{{this.methodName}}"), List(Literal(StringConstant(value)))) =>
extractClassNames(inner) :+ s"{{this.value}}[$value]"
{{/each}}
case Apply(Select(inner, "opacity"), List(Literal(IntConstant(value)))) =>
extractClassNames(inner).init :+ s"${extractClassNames(inner).last}/$value"
case Apply(Select(inner, "o"), List(Literal(IntConstant(value)))) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/scalawind/tests/cases/basic/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { generateContent } from '../../../src/generate'
import { tailwindConfig } from './config'

test('basic test', () => {
const actual = generateContent(tailwindConfig, "scalawind")
const actual = generateContent(tailwindConfig, "scalawind", false, true)
const expected = fs.readFileSync(path.join(__dirname, "./expected.txt"), "utf8")
expect(actual).toBe(expected)
})
3 changes: 2 additions & 1 deletion packages/scalawind/tests/cases/basic/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @type {import('tailwindcss').Config} */
export const tailwindConfig = {
theme: {
colors: {
Expand All @@ -8,6 +9,6 @@ export const tailwindConfig = {
'textColor',
'container',
'animation',
'width'
'width',
]
};
21 changes: 18 additions & 3 deletions packages/scalawind/tests/cases/basic/expected.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,13 @@ case class Tailwind() {
def important(@unused styles: Tailwind): Tailwind = Tailwind()
def i(@unused styles: Tailwind): Tailwind = Tailwind()
def raw(@unused classString: String): Tailwind = Tailwind()
def variant(selector: String, styles: Tailwind): Tailwind = Tailwind()
def opacity(value: Int): Tailwind = Tailwind()
def o(value: Int): Tailwind = Tailwind()
def variant(selector: String, styles: Tailwind): Tailwind = Tailwind()
}

object tw {
def apply(): Tailwind = Tailwind()


def container: Tailwind = Tailwind()
/** {{{
Expand Down Expand Up @@ -693,7 +692,10 @@ object tw {
* }}}
*/
def text_black: Tailwind = Tailwind()

def container_(value: String): Tailwind = Tailwind()
def w_(value: String): Tailwind = Tailwind()
def animate_(value: String): Tailwind = Tailwind()
def text_(value: String): Tailwind = Tailwind()
def first_letter(@unused styles: Tailwind): Tailwind = Tailwind()
def first_line(@unused styles: Tailwind): Tailwind = Tailwind()
def marker(@unused styles: Tailwind): Tailwind = Tailwind()
Expand Down Expand Up @@ -1379,6 +1381,11 @@ extension (tailwind: Tailwind)
* }}}
*/
def text_black: Tailwind = Tailwind()
def container_(value: String): Tailwind = Tailwind()
def w_(value: String): Tailwind = Tailwind()
def animate_(value: String): Tailwind = Tailwind()
def text_(value: String): Tailwind = Tailwind()


inline def sw(inline tailwind: Tailwind): String =
${ swImpl('tailwind) }
Expand Down Expand Up @@ -1879,6 +1886,14 @@ def swImpl(twStyleExpr: Expr[Tailwind])(using Quotes): Expr[String] = {
case Apply(Select(inner, "print"), List(styles)) =>
val classes = extractClassNames(styles).map(clx => s"print:$clx")
extractClassNames(inner) ++ classes
case Apply(Select(inner, "container_"), List(Literal(StringConstant(value)))) =>
extractClassNames(inner) :+ s"container-[$value]"
case Apply(Select(inner, "w_"), List(Literal(StringConstant(value)))) =>
extractClassNames(inner) :+ s"w-[$value]"
case Apply(Select(inner, "animate_"), List(Literal(StringConstant(value)))) =>
extractClassNames(inner) :+ s"animate-[$value]"
case Apply(Select(inner, "text_"), List(Literal(StringConstant(value)))) =>
extractClassNames(inner) :+ s"text-[$value]"
case Apply(Select(inner, "opacity"), List(Literal(IntConstant(value)))) =>
extractClassNames(inner).init :+ s"${extractClassNames(inner).last}/$value"
case Apply(Select(inner, "o"), List(Literal(IntConstant(value)))) =>
Expand Down
1 change: 1 addition & 0 deletions packages/scalawind/tests/cases/full/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @type {import('tailwindcss').Config} */
export const tailwindConfig = {
theme: {},
};
Loading