Skip to content

Commit

Permalink
feat/check duplication (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenyou authored Jun 8, 2024
1 parent 0fc14b3 commit 805ea2a
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-rockets-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"scalawind": minor
---

add class optimization for padding and margin utilities
2 changes: 1 addition & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "integration-test",
"type": "module",
"scripts": {
"generate": "scalawind generate -pcr -o ./src/scalawind.scala",
"generate": "scalawind generate --classes-validation -pcr -o ./src/scalawind.scala",
"test:scala": "scala-cli test src --test-only 'tests.only*'"
},
"dependencies": {
Expand Down
50 changes: 50 additions & 0 deletions integration/src/scalawind.test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,53 @@ class CreateNextAppHomePageTests extends munit.FunSuite {
)
}
}

class ClassesValidationTests extends munit.FunSuite {
test("duplication") {
assertNoDiff(
compileErrors("sw(tw.flex.flex)"),
"""|error: [Duplication] flex
| compileErrors("sw(tw.flex.flex)"),
| ^
|""".stripMargin
)

assertNoDiff(
compileErrors("sw(tw.flex.hover(tw.items_center.items_center))"),
"""|error: [Duplication] hover:items-center
| compileErrors("sw(tw.flex.hover(tw.items_center.items_center))"),
| ^
|""".stripMargin
)
}

test("optimization one-direction class to two-directions class") {
assertNoDiff(
compileErrors("sw(tw.mr_2.ml_2)"),
"""|error: [Optimization] Use mx-2 instead of ml-2 and mr-2
| compileErrors("sw(tw.mr_2.ml_2)"),
| ^
|""".stripMargin
)
}

test("optimization two-directions class to four-directions class") {
assertNoDiff(
compileErrors("sw(tw.mx_2.my_2)"),
"""|error: [Optimization] Use m-2 instead of mx-2 and my-2
| compileErrors("sw(tw.mx_2.my_2)"),
| ^
|""".stripMargin
)
}

test("optimization one-direction class to four-directions class") {
assertNoDiff(
compileErrors("sw(tw.ml_2.mr_2.mt_2.mb_2)"),
"""|error: [Optimization] Use m-2 instead of mt-2, mb-2, ml-2 and mr-2
| compileErrors("sw(tw.ml_2.mr_2.mt_2.mb_2)"),
| ^
|""".stripMargin
)
}
}
6 changes: 6 additions & 0 deletions packages/scalawind/src/commands/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const initOptionsSchema = z.object({
previewCompliedResult: z.boolean(),
laminar: z.boolean(),
scalajsReact: z.boolean(),
classesValidation: z.boolean()
})

export const generate = new Command()
Expand All @@ -36,6 +37,11 @@ export const generate = new Command()
"enable show preview compiled result",
false
)
.option(
"-cv, --classes-validation",
"enable classes validation",
false
)
.option(
"-l, --laminar",
"generate some helper methods for using with Laminar",
Expand Down
1 change: 1 addition & 0 deletions packages/scalawind/src/generate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Handlebars.registerPartial({
tailwind: Handlebars.compile(readTemplate('tailwind')),
laminar: Handlebars.compile(readTemplate('laminar')),
scalajsReact: Handlebars.compile(readTemplate('scalajsReact')),
classesValidation: Handlebars.compile(readTemplate('classesValidation')),
});

const template = Handlebars.compile(readTemplate('scalawind'));
Expand Down
68 changes: 68 additions & 0 deletions packages/scalawind/src/generate/templates/classesValidation.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{{#if classesValidation}}
def validate(classes: List[String]): Unit = {
checkDuplication(classes)
val optimizationSuggestions = checkOptimization(classes)
if optimizationSuggestions.nonEmpty then
report.errorAndAbort(s"[Optimization] ${optimizationSuggestions.mkString(", ")}")
}

def checkDuplication(classes: List[String]): Unit = {
val duplicates = classes.groupBy(identity).collect { case (x, List(_, _, _*)) => x }
if duplicates.nonEmpty then
report.errorAndAbort(s"[Duplication] ${duplicates.mkString(", ")}")
}

def checkOptimization(classes: List[String]): List[String] = {
val properties = List("p", "m")

val suggestions = properties.flatMap { property =>
val propertySuggestions = scala.collection.mutable.ListBuffer.empty[String]

val classMap = classes.map {
case c if c.startsWith(s"${property}t-") => s"${property}t" -> c
case c if c.startsWith(s"${property}b-") => s"${property}b" -> c
case c if c.startsWith(s"${property}l-") => s"${property}l" -> c
case c if c.startsWith(s"${property}r-") => s"${property}r" -> c
case c if c.startsWith(s"${property}x-") => s"${property}x" -> c
case c if c.startsWith(s"${property}y-") => s"${property}y" -> c
case c if c.startsWith(s"${property}-") => property -> c
case c => c -> c
}.groupBy(_._1).view.mapValues(_.map(_._2)).toMap

def checkAndSuggest(key1: String, key2: String, combined: String): Unit = {
(classMap.get(key1), classMap.get(key2)) match
case (Some(List(c1)), Some(List(c2))) if c1.substring(3) == c2.substring(3) =>
propertySuggestions += s"Use $combined${c1.substring(3)} instead of $c1 and $c2"
case _ => ()
}

def checkFourWay(): Unit = {
(classMap.get(s"${property}t"), classMap.get(s"${property}b"), classMap.get(s"${property}l"), classMap.get(s"${property}r")) match
case (Some(List(pt)), Some(List(pb)), Some(List(pl)), Some(List(pr))) if pt.substring(3) == pb.substring(3) && pl.substring(3) == pr.substring(3) && pt.substring(3) == pl.substring(3) =>
propertySuggestions += s"Use ${property}-${pt.substring(3)} instead of $pt, $pb, $pl and $pr"
case _ => ()
}

def checkPxPy(): Unit = {
(classMap.get(s"${property}x"), classMap.get(s"${property}y")) match
case (Some(List(px)), Some(List(py))) if px.substring(3) == py.substring(3) =>
propertySuggestions += s"Use ${property}-${px.substring(3)} instead of $px and $py"
case _ => ()
}

// Check for four-way combination first
checkFourWay()

// Only check for two-way combinations if no four-way combination is found
if propertySuggestions.isEmpty then
checkPxPy()
if propertySuggestions.isEmpty then
checkAndSuggest(s"${property}t", s"${property}b", s"${property}y-")
checkAndSuggest(s"${property}l", s"${property}r", s"${property}x-")

propertySuggestions.toList
}

suggestions
}
{{/if}}
3 changes: 2 additions & 1 deletion packages/scalawind/src/generate/templates/laminar.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{{#if laminar}}
import com.raquo.laminar.api.L.cls

object clx:
object clx {
def apply(styles: String) = cls(styles)
def :=(styles: String) = cls(styles)
}
{{/if}}
3 changes: 2 additions & 1 deletion packages/scalawind/src/generate/templates/scalajsReact.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{{#if scalajsReact}}
import japgolly.scalajs.react.vdom.html_<^.*

object clx:
object clx {
def apply(styles: String): TagMod = ^.cls := styles
def :=(styles: String): TagMod = ^.cls := styles
}
{{/if}}
25 changes: 16 additions & 9 deletions packages/scalawind/src/generate/templates/swMacro.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
implicit inline def sw(inline tailwind: Tailwind): String =
${ swImpl('tailwind) }

def methodNameToTailwindClass(rawName: String) =
def methodNameToTailwindClass(rawName: String) = {
val name = if rawName.startsWith("_") && rawName.charAt(1).isDigit then rawName.stripPrefix("_") else rawName
name.replace("_", "-").replace("per", "/").replace("dot", ".")
}

def swImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[String] =
def swImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[String] = {
import quotes.reflect.*

def extractClassNames(term: Term, prefix: String = "", important: Boolean = false): List[String] =
{{> classesValidation this }}

def extractClassNames(term: Term, prefix: String = "", important: Boolean = false): List[String] = {
var stack = List((term, prefix, important))
var classNames = List.empty[String]

Expand All @@ -18,7 +20,7 @@ def swImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[String] =
val (currentTerm, currentPrefix, currentImportant) = stack.head
stack = stack.tail

currentTerm match
currentTerm match {
case Apply(Select(inner, "important"), List(styles)) =>
stack = (styles, currentPrefix, true) :: stack
stack = (inner, currentPrefix, currentImportant) :: stack
Expand Down Expand Up @@ -70,14 +72,19 @@ def swImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[String] =
stack = (inner, currentPrefix, currentImportant) :: stack
case unexpectedTerm =>
report.errorAndAbort(s"Unexpected term: $unexpectedTerm")

}

classNames
end extractClassNames
}

val term = tailwindExpr.asTerm
val classNames = extractClassNames(term)
val combinedClasses = classNames.reverse.mkString(" ")
val classList = extractClassNames(term).reverse
{{#if classesValidation}}
validate(classList)
{{/if}}
val combinedClasses = classList.mkString(" ")
{{#if previewCompliedResult}}
report.info(s"$combinedClasses")
{{/if}}
Expr(combinedClasses)
}
5 changes: 3 additions & 2 deletions packages/scalawind/src/generate/templates/tailwind.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
val tw = Tailwind()

case class Tailwind():
case class Tailwind() {
{{#each modifiers}}
def {{this.name}}(@unused styles: Tailwind): Tailwind = this
{{/each}}
Expand All @@ -16,4 +16,5 @@ case class Tailwind():
{{/each}}
def important(@unused styles: Tailwind): Tailwind = this
def raw(@unused classString: String): Tailwind = this
def variant(selector: String, styles: Tailwind): Tailwind = this
def variant(selector: String, styles: Tailwind): Tailwind = this
}
1 change: 1 addition & 0 deletions packages/scalawind/tests/cases/basic/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test('basic test', () => {
previewCompliedResult: true,
laminar: false,
scalajsReact: true,
classesValidation: true
})
const filepath = path.join(__dirname, "./expected.txt")
// utils.writeFile(filepath, actual)
Expand Down
Loading

0 comments on commit 805ea2a

Please sign in to comment.