Skip to content

Commit

Permalink
v1.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenyou committed Dec 18, 2024
1 parent bba0ade commit f9c4cb5
Show file tree
Hide file tree
Showing 16 changed files with 668 additions and 30 deletions.
42 changes: 42 additions & 0 deletions .changeset/tall-files-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"scalawind": major
---

# Release 1.0

## Scala Source Transform

Previously, we need to configure Tailwind to scan the compiled JS files for extracting Tailwind classes. We have to do that because Tailwind doesn't understand our syntax. In this major release, we provide a transform source method to transform our Scala code into Tailwind classes so that Tailwind can understand and generate corresponding CSS classes.

```js
const { scalaSourceTransform } = require("scalawind/dist/transform");

/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["./index.html", "./src/**/*.scala"],
transform: scalaSourceTransform,
}
}

```

## Laminar Supports

We now have a more Laminar way for conditional styling.

### Signal[Boolean]

```scala
div(
tw.flex.items_center <-- booleanSignal
)
```

### Boolean

```scala
div(
tw.flex.items_center := boolean
)
```
4 changes: 2 additions & 2 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
- uses: VirtusLab/scala-cli-setup@main
with:
jvm: adopt:11
- name: Setup Node.js 20.x
- name: Setup Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x

- name: Install Dependencies & Build & Test Generator
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v4

- name: Setup Node.js 20.x
- name: Setup Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x

- name: Install Dependencies
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v4

- name: Setup Node.js 20.x
- name: Setup Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x

- name: Creating .npmrc
run: |
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
5 changes: 3 additions & 2 deletions examples/vite-app-mill/tailwind.config.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const colors = require("tailwindcss/colors");
const { scalaSourceTransform } = require("scalawind/dist/transform");

/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: [
'./index.html',
'./out/myapp/fastLinkJS.dest/**/*.js',
'./out/myapp/fullLinkJS.dest/**/*.js'
'./myapp/**/*.scala'
],
transform: scalaSourceTransform,
},
theme: {
colors: {
Expand Down
4 changes: 3 additions & 1 deletion examples/vite-app-sbt/tailwind.config.cjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const colors = require("tailwindcss/colors");
const { scalaSourceTransform } = require("scalawind/dist/transform");

/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ['./index.html', './target/scalajs-modules/**/*.js'],
files: ['./index.html', './src/**/*.scala'],
transform: scalaSourceTransform,
},
theme: {
colors: {
Expand Down
4 changes: 3 additions & 1 deletion examples/vite-app/tailwind.config.cjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const colors = require("tailwindcss/colors");
const { scalaSourceTransform } = require("scalawind/dist/transform");

/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ['./index.html', './scalajs-modules/**/*.js'],
files: ["./index.html", "./src/**/*.scala"],
transform: scalaSourceTransform,
},
theme: {
colors: {
Expand Down
1 change: 0 additions & 1 deletion packages/scalawind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"dist"
],
"main": "./dist/index.js",
"exports": "./dist/index.js",
"bin": "./dist/index.js",
"keywords": [
"scala",
Expand Down
44 changes: 41 additions & 3 deletions packages/scalawind/src/generate/templates/laminar.hbs
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
{{#if laminar}}
import com.raquo.laminar.api.L

implicit inline def lw(inline tailwind: Tailwind): L.Modifier[L.HtmlElement] = {
${ lwImpl('tailwind) }
extension (inline tailwind: Tailwind) {
inline def toHtmlMod: L.HtmlMod =
${ laminarTailwindImpl('tailwind) }
inline def toSvgMod: L.SvgMod =
${ laminarSvgTailwindImpl('tailwind) }
inline def <--(inline boolSignal: L.Signal[Boolean]): L.HtmlMod =
${ boolSignalClsImpl('tailwind, 'boolSignal) }
inline def :=(inline bool: Boolean): L.HtmlMod =
${ boolClsImpl('tailwind, 'bool) }
}

def lwImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[L.Modifier[L.HtmlElement]] = {
def boolSignalClsImpl(tailwindExpr: Expr[Tailwind], boolSignal: Expr[L.Signal[Boolean]])(using Quotes): Expr[L.HtmlMod] = {
val value = swImpl(tailwindExpr).valueOrAbort
'{ L.cls(${ Expr(value) }) <-- ${ boolSignal } }
}
def boolClsImpl(tailwindExpr: Expr[Tailwind], bool: Expr[Boolean])(using Quotes): Expr[L.HtmlMod] = {
val value = swImpl(tailwindExpr).valueOrAbort
'{ L.cls(${ Expr(value) }) := ${ bool } }
}

implicit inline def laminarTailwind(inline tailwind: Tailwind): L.HtmlMod = {
${ laminarTailwindImpl('tailwind) }
}
def laminarTailwindImpl(
tailwindExpr: Expr[Tailwind]
)(
using Quotes
): Expr[L.HtmlMod] = {
val value = swImpl(tailwindExpr).valueOrAbort
'{ L.cls := ${ Expr(value) } }
}

implicit inline def laminarSvgTailwind(inline tailwind: Tailwind): L.SvgMod = {
${ laminarSvgTailwindImpl('tailwind) }
}
def laminarSvgTailwindImpl(
tailwindExpr: Expr[Tailwind]
)(
using Quotes
): Expr[L.SvgMod] = {
val value = swImpl(tailwindExpr).valueOrAbort
'{ L.svg.className := ${ Expr(value) } }
}
{{/if}}
15 changes: 12 additions & 3 deletions packages/scalawind/src/generate/templates/scalajsReact.hbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
{{#if scalajsReact}}
import japgolly.scalajs.react.vdom.html_<^.*

implicit inline def cw(inline tailwind: Tailwind): TagMod = {
${ cwImpl('tailwind) }
extension (inline tailwind: Tailwind) {
inline def toTagMod: TagMod =
${ reactTailwindImpl('tailwind) }
}

def cwImpl(tailwindExpr: Expr[Tailwind])(using Quotes): Expr[TagMod] = {
implicit inline def reactTailwind(inline tailwind: Tailwind): TagMod = {
${ reactTailwindImpl('tailwind) }
}

def reactTailwindImpl(
tailwindExpr: Expr[Tailwind]
)(
using Quotes
): Expr[TagMod] = {
val value = swImpl(tailwindExpr).valueOrAbort
'{ ^.cls := ${ Expr(value) } }
}
Expand Down
143 changes: 143 additions & 0 deletions packages/scalawind/src/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@

const PATTERNS = {
TW: /tw(?:\s*\.(?:`[a-zA-Z0-9_/.]+`|[a-zA-Z0-9_¥#/\[\]§"]+)|\s*\((?:[^)(]+|\((?:[^)(]+|\((?:[^)(]+|\((?:[^)(]+|\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\))*\))*\))*\))*\))+/g,
TW_REPLACE: /tw\.|\.|_|`|§|"/g,
MULTILINE: /\n\s+(?=(?:[^"]*"[^"]*")*[^"]*$)/g,
BACKTICK: /`([^`]*)`/g,
ARBITRARY: /_\(("[^"]+")?\)/g,
OPACITY: /\$\("([^"]+)"?\)/g,
VARIANT_TRANSFORM: /variant\("([^"]+)",\s*(tw\.[^)]+)\)/g,
};

const REPLACEMENTS = {
'tw.': '',
'.': ' ',
'_': '-',
'`': '',
'§': '.',
'"': '',
};

function replaceSpecialCases(source) {
return source.replace(/_2xl/g, '2xl');
}

function handleArbitraryValues(source) {
return source.replace(PATTERNS.ARBITRARY, (match, content) => {
if (content) {
return '_¥' + content.replace(/_/g, '⌇') + '¥';
}
return match;
});
}

function handleOpacityValues(source) {
return source.replace(PATTERNS.OPACITY, (match, content) => {
if (content) {
content = content.replace(/\./g, '§');
return '/€' + content + '€';
}
return match;
});
}

function handleVariantTransform(source) {
return source.replace(PATTERNS.VARIANT_TRANSFORM, (match, selector, content) => {
const safeSelector = selector.replace(/[().]/g, char => ({
'(': '《',
')': '》',
'.': '§'
})[char]);
return `[${safeSelector}](${content})`;
});
}

function flattenClasses(twString) {
const classes = [];
const stack = [];
let currentClass = "";

for (const char of twString) {
if (char === "(") {
if (currentClass) {
stack.push(currentClass);
currentClass = "";
}
} else if (char === ")") {
if (currentClass) {
classes.push(`${stack.join(":")}:${currentClass}`);
currentClass = "";
}
stack.pop();
} else if (char === " ") {
if (currentClass) {
classes.push(stack.length ? `${stack.join(":")}:${currentClass}` : currentClass);
currentClass = "";
}
} else {
currentClass += char;
}
}

if (currentClass) {
classes.push(stack.length ? `${stack.join(":")}:${currentClass}` : currentClass);
}

return classes.join(" ")
.replace(/¥([^¥]+)¥/g, '[$1]')
.replace(/([^]+)/g, '$1')
.replace(/important:|||raw:|/g, match => ({
'important:': '!',
'《': '(',
'》': ')',
'raw:': '',
'⌇': '_'
})[match])
}

function preprocessSource(source) {
try {
let processed = replaceSpecialCases(source);
processed = handleArbitraryValues(processed);
processed = handleOpacityValues(processed);
return processed;
} catch (error) {
console.error('Error preprocessing source:', error);
throw error;
}
}

function extractTw(source) {
try {
const preprocessed = preprocessSource(source);
const matches = preprocessed.match(PATTERNS.TW);

if (!matches) {
return [];
}

return matches.map(match => {
return flattenClasses(handleVariantTransform(match).replace(PATTERNS.MULTILINE, '').replace(PATTERNS.BACKTICK, (_, content) => {
return '`' + content.replace(/\./g, '§') + '`';
}).replace(PATTERNS.TW_REPLACE, match => REPLACEMENTS[match]));
});
} catch (error) {
console.error('Error extracting tw:', error);
throw error;
}
}

export function transformSource(source) {
try {
return extractTw(source).join(" ");
} catch (error) {
console.error('Error transforming source:', error);
throw error;
}
}

export const scalaSourceTransform = {
scala: (content) => {
return transformSource(content)
}
}
Loading

0 comments on commit f9c4cb5

Please sign in to comment.