Skip to content

Commit

Permalink
chore: refactor existing code based on pull request feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
fdukat committed Jul 5, 2023
1 parent fcad390 commit def31cb
Show file tree
Hide file tree
Showing 8 changed files with 758 additions and 240 deletions.
656 changes: 656 additions & 0 deletions __stubs__/asts.ts

Large diffs are not rendered by default.

59 changes: 25 additions & 34 deletions src/ASTtoCSS.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-prototype-builtins */
import { Node } from "@babel/core"
import * as t from "@babel/types"

Expand All @@ -20,50 +19,42 @@ export function generateCSSFromAST(ast: Node): string {

function traverseAST(node: Node, selectorPrefix = "") {
if (t.isTaggedTemplateExpression(node)) {
const { quasi } = node
const templateElements = quasi.quasis.map((element) => element.value.raw)
const cssRule = templateElements.join("").trim()
const cssRule = node.quasi.quasis
.map((element) => element.value.raw)
.join("")
.trim()

if (cssRule.length > 0) {
let randomSelector = generateRandomSelector()
while (generatedSelectors.has(randomSelector)) {
let randomSelector

do {
randomSelector = generateRandomSelector()
}
} while (generatedSelectors.has(randomSelector))

const selector = `${selectorPrefix}.${randomSelector}`
generatedSelectors.add(randomSelector)
cssCode += `${selector} { ${cssRule} }\n`
}
}

if (node && typeof node === "object") {
for (const key in node) {
if (node.hasOwnProperty(key)) {
const childNode = node[key]
if (Array.isArray(childNode)) {
childNode.forEach((child) => traverseAST(child, selectorPrefix))
} else if (childNode && typeof childNode === "object") {
if (t.isJSXElement(childNode)) {
const { openingElement, closingElement } = childNode
const { name } = openingElement
if (t.isJSXIdentifier(name)) {
const childSelector = `${selectorPrefix} ${name.name}`
traverseAST(childNode, childSelector)
}
if (closingElement) {
const { name: closingName } = closingElement
if (t.isJSXIdentifier(closingName)) {
const childSelector = `${selectorPrefix} ${closingName.name}`
traverseAST(closingElement, childSelector)
}
}
} else {
traverseAST(childNode, selectorPrefix)
}
}
}
Object.values(node).forEach((childNode) => {
if (!childNode || typeof childNode !== "object") return

if (Array.isArray(childNode)) {
childNode.forEach((child) => traverseAST(child, selectorPrefix))
} else if (t.isJSXElement(childNode)) {
const { openingElement, closingElement } = childNode

;[openingElement, closingElement].forEach((element) => {
if (!element || !t.isJSXIdentifier(element.name)) return

const childSelector = `${selectorPrefix} ${element.name.name}`
traverseAST(element, childSelector)
})
} else {
traverseAST(childNode, selectorPrefix)
}
}
})
}

traverseAST(ast)
Expand Down
59 changes: 20 additions & 39 deletions src/CSStoTailwind.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,36 @@
import _ from "lodash"

export function convertToTailwindCSS(cssCode: string): string {
const cssRules = cssCode.split("}").map((rule) => rule.trim())
let tailwindClasses = ""
const CSS_BLOCKS = cssCode.split("}").map((block) => block.trim())

const tailwindClasses = CSS_BLOCKS.filter((block) => block.includes("{")).map((block) => {
const [selectors, declarations] = block.split("{").map((part) => part.trim())
const selectorClasses = extractSelectorClasses(selectors)
const declarationClasses = extractDeclarationClasses(declarations)

cssRules.forEach((rule) => {
if (rule.includes("{")) {
const [selectors, declarations] = rule.split("{").map((part) => part.trim())
const selectorClasses = getSelectorClasses(selectors)
const declarationClasses = getDeclarationClasses(declarations)
if (selectorClasses !== "") {
tailwindClasses += `${selectorClasses} { ${declarationClasses} }\n\n`
} else {
tailwindClasses += `${declarationClasses}\n\n`
}
}
return selectorClasses ? `${selectorClasses} { ${declarationClasses.join(" ")} }` : declarationClasses.join(" ")
})

return tailwindClasses.trim()
return tailwindClasses.join(" ")
}

function getSelectorClasses(selectors: string): string {
function extractSelectorClasses(selectors: string): string {
const classes = selectors.split(",").map((item) => item.trim())

return classes.join(", ")
}

function getDeclarationClasses(declarations: string): string {
function extractDeclarationClasses(declarations: string): string[] {
const properties = declarations.split(";").map((declaration) => declaration.trim())
let classes = ""

properties.forEach((property) => {
if (property.includes(":")) {
return properties
.filter((property) => property.includes(":"))
.map((property) => {
const [propertyKey, propertyValue] = property.split(":").map((part) => part.trim())
const tailwindClass = getTailwindClass(propertyKey, propertyValue)
if (tailwindClass) {
classes += `${tailwindClass} `
} else {
classes += `${getPropertyAlias(propertyKey)}-${wrapWithArbitraryValue(propertyValue)} `
}
}
})

return classes.trim()
return tailwindClass ? tailwindClass : `${getPropertyAlias(propertyKey)}-${wrapWithArbitraryValue(propertyValue)}`
})
}

function getTailwindClass(property: string, value: string): string | null {
Expand Down Expand Up @@ -69,12 +60,9 @@ function getTailwindClass(property: string, value: string): string | null {
},
}

const propertyMapping = propertyMappings[property] || propertyMappings[toCamelCase(property)]
if (propertyMapping && propertyMapping[value]) {
return propertyMapping[value]
}
const propertyMapping = propertyMappings[property] || propertyMappings[_.camelCase(property)]

return null
return propertyMapping && propertyMapping[value] ? propertyMapping[value] : null
}

function getPropertyAlias(property: string): string {
Expand All @@ -83,9 +71,6 @@ function getPropertyAlias(property: string): string {
color: "text",
fontSize: "text",
fontWeight: "font",
// Add more property aliases here
// Example:
// textDecoration: "underline",
}

return propertyAliases[property] || property
Expand All @@ -94,7 +79,3 @@ function getPropertyAlias(property: string): string {
function wrapWithArbitraryValue(value: string): string {
return `[${value}]`
}

function toCamelCase(str: string): string {
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
}
4 changes: 2 additions & 2 deletions src/styledComponentsToAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function transformCodeToAST(input: string) {
throw new Error("Provided input is empty")
}

let ast
let ast: t.Node
try {
ast = parse(input, {
sourceType: "module",
Expand Down Expand Up @@ -45,7 +45,7 @@ export function transformCodeToAST(input: string) {
const { declarations } = path.node

declarations.forEach((declaration) => {
const { id, init } = declaration
const { init } = declaration

if (t.isCallExpression(init) && t.isIdentifier(init.callee, { name: styledIdentifier.name })) {
const styledCallExpression = t.callExpression(
Expand Down
179 changes: 26 additions & 153 deletions test/ASTtoCSS.test.js
Original file line number Diff line number Diff line change
@@ -1,177 +1,50 @@
import { describe, it, expect } from "vitest"
import { generateCSSFromAST } from "../src/ASTtoCSS"
import { stripWhitespace } from "./test-utils"

const input = {
type: "File",
start: 0,
end: 265,
loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } },
errors: [],
program: {
type: "Program",
start: 0,
end: 265,
loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } },
sourceType: "module",
interpreter: null,
body: [
{
type: "ImportDeclaration",
start: 7,
end: 45,
loc: { start: { line: 2, column: 6, index: 7 }, end: { line: 2, column: 44, index: 45 } },
importKind: "value",
specifiers: [
{
type: "ImportDefaultSpecifier",
start: 14,
end: 20,
loc: { start: { line: 2, column: 13, index: 14 }, end: { line: 2, column: 19, index: 20 } },
local: {
type: "Identifier",
start: 14,
end: 20,
loc: {
start: { line: 2, column: 13, index: 14 },
end: { line: 2, column: 19, index: 20 },
identifierName: "styled",
},
name: "styled",
},
},
{
type: "ImportSpecifier",
local: { type: "Identifier", name: "styled" },
imported: { type: "Identifier", name: "styled" },
},
],
source: {
type: "StringLiteral",
start: 26,
end: 45,
loc: { start: { line: 2, column: 25, index: 26 }, end: { line: 2, column: 44, index: 45 } },
extra: { rawValue: "styled-components", raw: "'styled-components'" },
value: "styled-components",
},
},
{
type: "VariableDeclaration",
start: 53,
end: 260,
loc: { start: { line: 4, column: 6, index: 53 }, end: { line: 12, column: 7, index: 260 } },
declarations: [
{
type: "VariableDeclarator",
start: 59,
end: 260,
loc: { start: { line: 4, column: 12, index: 59 }, end: { line: 12, column: 7, index: 260 } },
id: {
type: "Identifier",
start: 59,
end: 65,
loc: {
start: { line: 4, column: 12, index: 59 },
end: { line: 4, column: 18, index: 65 },
identifierName: "Button",
},
name: "Button",
},
init: {
type: "TaggedTemplateExpression",
start: 68,
end: 260,
loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 12, column: 7, index: 260 } },
tag: {
type: "MemberExpression",
start: 68,
end: 81,
loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 4, column: 34, index: 81 } },
object: {
type: "Identifier",
start: 68,
end: 74,
loc: {
start: { line: 4, column: 21, index: 68 },
end: { line: 4, column: 27, index: 74 },
identifierName: "styled",
},
name: "styled",
},
computed: false,
property: {
type: "Identifier",
start: 75,
end: 81,
loc: {
start: { line: 4, column: 28, index: 75 },
end: { line: 4, column: 34, index: 81 },
identifierName: "button",
},
name: "button",
},
},
quasi: {
type: "TemplateLiteral",
start: 81,
end: 260,
loc: { start: { line: 4, column: 34, index: 81 }, end: { line: 12, column: 7, index: 260 } },
expressions: [],
quasis: [
{
type: "TemplateElement",
start: 82,
end: 259,
loc: { start: { line: 4, column: 35, index: 82 }, end: { line: 12, column: 6, index: 259 } },
value: {
raw: "\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ",
cooked:
"\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ",
},
tail: true,
},
],
},
},
},
],
kind: "const",
},
],
directives: [],
},
comments: [],
}
import { regularAST, multipleDeclarationsAST, nestedTemplateAST } from "../__stubs__/asts"

describe("#generateCSSFromAST", () => {
it("Should generate valid CSS from correct AST input", () => {
const result = stripWhitespace(generateCSSFromAST(input))
it("should generate valid CSS from correct AST input", () => {
const result = stripWhitespace(generateCSSFromAST(regularAST))

expect(result).toEqual(
expect.stringContaining(
"{background:white;color:palevioletred;font-size:1em;&:hover{background:palevioletred;color:white;}}"
"background:white;color:palevioletred;font-size:1em;&:hover{background:palevioletred;color:white;"
)
)
})

it("Should generate valid CSS from empty AST input", () => {
const input = ``
it("should generate valid CSS from empty AST input", () => {
const input = ""

const result = stripWhitespace(generateCSSFromAST(input))

expect(result).toEqual("") // Expect an empty string since there are no CSS rules
})

it("Should generate valid CSS from AST input with multiple tagged template expressions", () => {
const result = stripWhitespace(generateCSSFromAST(input))
it("should generate valid CSS from AST input with multiple tagged template expressions", () => {
const result = stripWhitespace(generateCSSFromAST(multipleDeclarationsAST))

expect(result).toEqual(
expect.stringContaining(
stripWhitespace("background:;color:;font-size:;padding:0.25em1em;border:2pxsolid;border-radius:3px;")
)
)

expect(result).toEqual(expect.stringContaining("background:white;color:palevioletred;font-size:1em;"))
expect(result).toEqual(expect.stringContaining("&:hover{background:palevioletred;color:white;}"))
expect(result).toEqual(expect.stringContaining(stripWhitespace("font-size:1rem;padding:4rem;")))
})

it("Should generate valid CSS from AST input with nested tagged template expressions", () => {
const result = stripWhitespace(generateCSSFromAST(input))
it("should generate valid CSS from AST input with nested tagged template expressions", () => {
const result = stripWhitespace(generateCSSFromAST(nestedTemplateAST))

expect(result).toEqual(
expect.stringContaining(
stripWhitespace(
"display: flex; justify-content:center;align-items:center;height:100vh;background:linear-gradient(145deg,rgba(253, 38, 71, 1) 0%,rgba(252, 128, 45, 1)75%,rgba(250, 167, 43, 1)100%);"
)
)
)

expect(result).toEqual(expect.stringContaining("&:hover{background:palevioletred;color:white;}"))
expect(result).toEqual(expect.stringContaining(stripWhitespace("margin-bottom: 2em;")))
})
})
Loading

0 comments on commit def31cb

Please sign in to comment.