forgecss 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/fx.js CHANGED
@@ -1,16 +1,74 @@
1
1
  export default function fx(classes) {
2
- return classes
3
- .split(" ")
4
- .map((className) => {
5
- const [label, rest] = className.split(":");
6
- if (!rest) return label;
2
+ return parseClass(classes).map((className) => {
3
+ let [label, rest] = splitClassName(className);
4
+ if (!label || label === "[true]") return rest;
5
+ if (label === "[false]") return false;
6
+ label = normalizeLabel(label);
7
7
  return rest
8
8
  .split(",")
9
- .map((cls) => `${label}_${cls}`)
10
- .filter(Boolean)
9
+ .map((cls) => `${label}--${cls}`)
11
10
  .join(" ");
12
11
  })
13
12
  .filter(Boolean)
14
13
  .join(" ");
15
- };
14
+ }
15
+ export function splitClassName(label) {
16
+ const lastColonIndex = label.lastIndexOf(":");
17
+ if (lastColonIndex === -1) {
18
+ return [null, label];
19
+ }
20
+ const prefix = label.slice(0, lastColonIndex);
21
+ const rest = label.slice(lastColonIndex + 1);
22
+ return [prefix, rest];
23
+ }
24
+
25
+ export function normalizeLabel(label) {
26
+ let normalized = label.trim();
27
+ normalized = normalized.replace(/[&]/g, "I");
28
+ normalized = normalized.replace(/[:| =]/g, "-");
29
+ normalized = normalized.replace(/[^a-zA-Z0-9_-]/g, '');
30
+ return normalized;
31
+ }
32
+
33
+ export function parseClass(str) {
34
+ const out = [];
35
+ let buf = "";
36
+
37
+ let depth = 0;
38
+ let quote = null;
39
+ for (let i = 0; i < str.length; i++) {
40
+ const ch = str[i];
41
+ if (depth > 0) {
42
+ if (quote) {
43
+ buf += ch;
44
+ if (ch === quote && str[i - 1] !== "\\") quote = null;
45
+ continue;
46
+ } else if (ch === "'" || ch === '"') {
47
+ quote = ch;
48
+ buf += ch;
49
+ continue;
50
+ }
51
+ }
52
+ if (ch === "[") {
53
+ depth++;
54
+ buf += ch;
55
+ continue;
56
+ }
57
+ if (ch === "]" && depth > 0) {
58
+ depth--;
59
+ buf += ch;
60
+ continue;
61
+ }
62
+ if (depth === 0 && /\s/.test(ch)) {
63
+ if (buf) out.push(buf);
64
+ buf = "";
65
+ while (i + 1 < str.length && /\s/.test(str[i + 1])) i++;
66
+ continue;
67
+ }
68
+ buf += ch;
69
+ }
70
+
71
+ if (buf) out.push(buf);
72
+ return out;
73
+ }
16
74
 
@@ -1 +1 @@
1
- (()=>{function o(i){return i.split(" ").map(s=>{let[e,t]=s.split(":");return t?t.split(",").map(n=>`${e}_${n}`).filter(Boolean).join(" "):e}).filter(Boolean).join(" ")}function r(i){for(var s=i||document,e=s.querySelectorAll("[class]"),t=0;t<e.length;t++){var n=e[t],a=n.getAttribute("class");if(a){var f=o(a);typeof f=="string"&&f!==a&&n.setAttribute("class",f)}}}window.fx=o;window.forgecss=r;document.readyState!=="loading"?r():document.addEventListener("DOMContentLoaded",function(){r()});window.addEventListener("load",function(){r()});})();
1
+ (()=>{function s(n){return a(n).map(t=>{let[e,i]=f(t);return!e||e==="[true]"?i:e==="[false]"?!1:(e=c(e),i.split(",").map(l=>`${e}--${l}`).join(" "))}).filter(Boolean).join(" ")}function f(n){let t=n.lastIndexOf(":");if(t===-1)return[null,n];let e=n.slice(0,t),i=n.slice(t+1);return[e,i]}function c(n){let t=n.trim();return t=t.replace(/[&]/g,"I"),t=t.replace(/[:| =]/g,"-"),t=t.replace(/[^a-zA-Z0-9_-]/g,""),t}function a(n){let t=[],e="",i=0,l=null;for(let r=0;r<n.length;r++){let o=n[r];if(i>0){if(l){e+=o,o===l&&n[r-1]!=="\\"&&(l=null);continue}else if(o==="'"||o==='"'){l=o,e+=o;continue}}if(o==="["){i++,e+=o;continue}if(o==="]"&&i>0){i--,e+=o;continue}if(i===0&&/\s/.test(o)){for(e&&t.push(e),e="";r+1<n.length&&/\s/.test(n[r+1]);)r++;continue}e+=o}return e&&t.push(e),t}function u(n){for(var t=n||document,e=t.querySelectorAll("[class]"),i=0;i<e.length;i++){var l=e[i],r=l.getAttribute("class");if(r){var o=s(r);typeof o=="string"&&o!==r&&l.setAttribute("class",o)}}}window.fx=s;window.forgecss=u;document.readyState!=="loading"?u():document.addEventListener("DOMContentLoaded",function(){u()});window.addEventListener("load",function(){u()});})();
package/index.d.ts CHANGED
@@ -2,10 +2,8 @@ export type ForgeCSSOptions = {
2
2
  inventoryFiles?: string[];
3
3
  usageFiles?: string[];
4
4
  usageAttributes?: string[];
5
- mapping?: {
6
- queries?: {
7
- [key: string]: string
8
- };
5
+ breakpoints?: {
6
+ [key: string]: string
9
7
  };
10
8
  };
11
9
 
package/index.js CHANGED
@@ -1,24 +1,20 @@
1
1
  import { writeFile } from "fs/promises";
2
2
  import getAllFiles from "./lib/getAllFiles.js";
3
3
  import { extractStyles, invalidateInvetory } from "./lib/inventory.js";
4
- import { invalidateUsageCache, findUsages } from "./lib/processor.js";
4
+ import { invalidateUsageCache, findUsages } from "./lib/usages.js";
5
5
  import { generateOutputCSS } from "./lib/generator.js";
6
6
 
7
7
  const DEFAULT_OPTIONS = {
8
8
  inventoryFiles: ["css", "less", "scss"],
9
9
  usageFiles: ["html", "jsx", "tsx"],
10
10
  usageAttributes: ["class", "className"],
11
- mapping: {
12
- queries: {}
13
- }
11
+ breakpoints: {}
14
12
  };
15
13
 
16
14
  export default function ForgeCSS(options) {
17
15
  const config = { ...DEFAULT_OPTIONS };
18
16
 
19
- config.mapping = {
20
- queries: Object.assign({}, DEFAULT_OPTIONS.mapping.queries, options?.mapping?.queries ?? {})
21
- };
17
+ config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {});
22
18
 
23
19
  async function result(output) {
24
20
  try {
package/lib/generator.js CHANGED
@@ -1,6 +1,8 @@
1
- import { getUsages } from "./processor.js";
1
+ import { getUsages } from "./usages.js";
2
+ import arbitraryTransformer from "./transformers/arbitrary.js";
2
3
  import mediaQueryTransformer from "./transformers/mediaQuery.js";
3
4
  import pseudoClassTransformer from "./transformers/pseudo.js";
5
+ import { resolveApplys } from "./inventory.js";
4
6
 
5
7
  export async function generateOutputCSS(config) {
6
8
  const bucket = {};
@@ -12,6 +14,8 @@ export async function generateOutputCSS(config) {
12
14
  return;
13
15
  } else if (pseudoClassTransformer(label, usages[file][label], bucket)) {
14
16
  return;
17
+ } else if (arbitraryTransformer(label, usages[file][label], bucket)) {
18
+ return;
15
19
  }
16
20
  } catch (err) {
17
21
  console.error(
@@ -24,6 +28,7 @@ export async function generateOutputCSS(config) {
24
28
  }
25
29
  });
26
30
  });
31
+ resolveApplys(bucket);
27
32
  return Object.keys(bucket)
28
33
  .map((key) => {
29
34
  if (bucket[key].rules) {
package/lib/helpers.js ADDED
@@ -0,0 +1,19 @@
1
+ import postcss from "postcss";
2
+ import { getStylesByClassName } from "./inventory.js";
3
+
4
+ export function setDeclarations(selector, rule) {
5
+ const decls = getStylesByClassName(selector);
6
+ if (decls.length === 0) {
7
+ console.warn(`forgecss: no class ".${selector}" found`);
8
+ return;
9
+ }
10
+ decls.forEach((d) => {
11
+ rule.append(
12
+ postcss.decl({
13
+ prop: d.prop,
14
+ value: d.value,
15
+ important: d.important
16
+ })
17
+ );
18
+ });
19
+ }
@@ -1,3 +1,4 @@
1
1
  export function extractStyles(filePath: string, css?: string | null): Promise<void>;
2
2
  export function getStylesByClassName(selector: string): object[];
3
3
  export function invalidateInvetory(): void;
4
+ export function resolveApplys(bucket: object): void
package/lib/inventory.js CHANGED
@@ -19,8 +19,37 @@ export function getStylesByClassName(selector) {
19
19
  }
20
20
  });
21
21
  });
22
+ if (decls.length === 0) {
23
+ console.warn(`forgecss: Warning - no styles found for class "${selector}".`);
24
+ }
22
25
  return decls;
23
26
  }
24
27
  export function invalidateInvetory() {
25
28
  INVENTORY = {};
29
+ }
30
+ export function resolveApplys(bucket) {
31
+ Object.keys(INVENTORY).forEach((filePath) => {
32
+ INVENTORY[filePath].walkRules((rule) => {
33
+ rule.walkDecls((d) => {
34
+ if (d.prop === '--apply') {
35
+ const classesToApply = d.value.split(' ').map(c => c.trim()).filter(Boolean);
36
+ const newRule = postcss.rule({ selector: rule.selector });
37
+ classesToApply.forEach((className) => {
38
+ const styles = getStylesByClassName(className);
39
+ styles.forEach((style) => {
40
+ newRule.append({
41
+ prop: style.prop,
42
+ value: style.value,
43
+ important: style.important
44
+ });
45
+ });
46
+ });
47
+ if (!bucket['_APPLY_']) {
48
+ bucket["_APPLY_"] = postcss.root();
49
+ }
50
+ bucket["_APPLY_"].append(newRule);
51
+ }
52
+ });
53
+ });
54
+ });
26
55
  }
@@ -0,0 +1,32 @@
1
+ import postcss from "postcss";
2
+
3
+ import { normalizeLabel } from "../../client/fx.js";
4
+ import { setDeclarations } from "../helpers.js";
5
+
6
+ export default function arbitraryTransformer(label, classes, bucket) {
7
+ if (label.startsWith("[") && label.endsWith("]")) {
8
+ let arbitrarySelector = label.slice(1, -1).trim();
9
+ if (['', 'true'].includes(arbitrarySelector)) {
10
+ return true;
11
+ }
12
+ classes.forEach((cls) => {
13
+ const I = normalizeLabel(label) + "--" + cls;
14
+ const selector = evaluateArbitrary(arbitrarySelector, I);
15
+ const root = postcss.root();
16
+ if (bucket[I]) {
17
+ return;
18
+ }
19
+ const rule = postcss.rule({ selector });
20
+ setDeclarations(cls, rule);
21
+ root.append(rule);
22
+ bucket[I] = root;
23
+ });
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function evaluateArbitrary(label, I) {
30
+ label = label.replace(/[&]/g, `.${I}`);
31
+ return label;
32
+ }
@@ -1,43 +1,30 @@
1
1
  import postcss from "postcss";
2
2
 
3
- import { getStylesByClassName } from "../inventory.js";
3
+ import { setDeclarations } from "../helpers.js";
4
+ import {normalizeLabel} from "../../client/fx.js";
4
5
 
5
- export default function mediaQueryTransformer(config, label, selectors, bucket) {
6
- if (!config?.mapping?.queries[label]) {
6
+ export default function mediaQueryTransformer(config, label, classes, bucket) {
7
+ if (!config?.breakpoints[label]) {
7
8
  return false;
8
9
  }
9
10
  if (!bucket[label]) {
10
11
  bucket[label] = {
11
12
  rules: postcss.atRule({
12
13
  name: "media",
13
- params: `all and (${config.mapping.queries[label]})`
14
+ params: `all and (${config.breakpoints[label]})`
14
15
  }),
15
16
  classes: {}
16
17
  };
17
18
  }
18
19
  const rules = bucket[label].rules;
19
- selectors.forEach((selector) => {
20
- const prefixedSelector = `.${label}_${selector}`;
21
- if (bucket[label].classes[prefixedSelector]) {
20
+ classes.forEach((cls) => {
21
+ const selector = `.${normalizeLabel(label)}--${cls}`;
22
+ if (bucket[label].classes[selector]) {
22
23
  return;
23
24
  }
24
- bucket[label].classes[prefixedSelector] = true; // caching
25
- const rule = postcss.rule({ selector: prefixedSelector });
26
- const decls = getStylesByClassName(selector);
27
- if (decls.length === 0) {
28
- console.warn(`forgecss: no styles found for class ".${selector}" used in media query "${label}"`);
29
- delete bucket[label];
30
- return;
31
- }
32
- decls.forEach((d) => {
33
- rule.append(
34
- postcss.decl({
35
- prop: d.prop,
36
- value: d.value,
37
- important: d.important
38
- })
39
- );
40
- });
25
+ bucket[label].classes[selector] = true; // caching
26
+ const rule = postcss.rule({ selector });
27
+ setDeclarations(cls, rule);
41
28
  rules.append(rule);
42
29
  });
43
30
  return true;
@@ -1,5 +1,7 @@
1
1
  import postcss from "postcss";
2
- import { getStylesByClassName } from "../inventory.js";
2
+
3
+ import {setDeclarations} from "../helpers.js";
4
+ import {normalizeLabel} from "../../client/fx.js";
3
5
 
4
6
  const ALLOWED_PSEUDO_CLASSES = [
5
7
  "hover",
@@ -24,34 +26,21 @@ const ALLOWED_PSEUDO_CLASSES = [
24
26
  "user-invalid"
25
27
  ];
26
28
 
27
- export default function pseudoClassTransformer(label, selectors, bucket) {
28
- if (!ALLOWED_PSEUDO_CLASSES.includes(label)) {
29
- return false;
30
- }
31
- const root = postcss.root();
32
- selectors.forEach((selector) => {
33
- const key = `${label}_${selector}`;
34
- if (bucket[key]) {
35
- // already have that
36
- return;
37
- }
38
- const rule = postcss.rule({ selector: `.${key}:${label}` });
39
- const decls = getStylesByClassName(selector);
40
- if (decls.length === 0) {
41
- console.warn(`forgecss: no styles found for class ".${selector}" used in pseudo class "${label}"`);
42
- return;
43
- }
44
- decls.forEach((d) => {
45
- rule.append(
46
- postcss.decl({
47
- prop: d.prop,
48
- value: d.value,
49
- important: d.important
50
- })
51
- );
29
+ export default function pseudoClassTransformer(label, classes, bucket) {
30
+ if (ALLOWED_PSEUDO_CLASSES.includes(label)) {
31
+ classes.forEach((cls) => {
32
+ const selector = `.${normalizeLabel(label)}--${cls}:${label}`;
33
+ const root = postcss.root();
34
+ if (bucket[selector]) {
35
+ // already have that
36
+ return;
37
+ }
38
+ const rule = postcss.rule({ selector });
39
+ setDeclarations(cls, rule);
40
+ root.append(rule);
41
+ bucket[selector] = root;
52
42
  });
53
- root.append(rule);
54
- bucket[key] = root;
55
- });
56
- return true;
43
+ return true;
44
+ }
45
+ return false;
57
46
  }
@@ -1,7 +1,8 @@
1
1
  import swc from "@swc/core";
2
- import { readFile } from "fs/promises";
2
+ import { readFile, writeFile } from "fs/promises";
3
3
  import { fromHtml } from "hast-util-from-html";
4
4
  import { visit } from "unist-util-visit";
5
+ import { parseClass } from "../client/fx.js";
5
6
 
6
7
  const FUNC_NAME = 'fx';
7
8
  let USAGES = {};
@@ -35,6 +36,7 @@ export async function findUsages(filePath, fileContent = null) {
35
36
  tsx: true,
36
37
  decorators: false
37
38
  });
39
+ // writeFile(process.cwd() + '/ast.json', JSON.stringify(ast, null, 2), 'utf-8').catch(() => {});
38
40
  traverseASTNode(ast, {
39
41
  JSXExpressionContainer(node) {
40
42
  if (node?.expression?.callee?.value === FUNC_NAME && node?.expression?.arguments) {
@@ -42,10 +44,8 @@ export async function findUsages(filePath, fileContent = null) {
42
44
  const arg = node.expression.arguments[0];
43
45
  let value = arg?.expression.value;
44
46
  if (arg.expression.type === "TemplateLiteral") {
45
- value = "";
46
- arg.expression.quasis.forEach((elem) => {
47
- value += elem?.cooked || "";
48
- });
47
+ let quasis = arg.expression.quasis.map((elem) => elem?.cooked || "");
48
+ value = quasis.join("");
49
49
  }
50
50
  storeUsage(filePath, value);
51
51
  }
@@ -69,20 +69,25 @@ export function getUsages() {
69
69
  return USAGES;
70
70
  }
71
71
  function storeUsage(filePath, classesString = "") {
72
- if (classesString) {
73
- classesString.split(" ").forEach((part) => {
74
- if (part.indexOf(":") > -1) {
75
- let [label, classes] = part.split(":");
76
- classes = classes.split(",");
77
- classes.forEach((cls) => {
78
- if (!USAGES[filePath][label]) {
79
- USAGES[filePath][label] = [];
80
- }
81
- USAGES[filePath][label].push(cls);
82
- });
72
+ if (!classesString) return;
73
+
74
+ parseClass(classesString).forEach((part) => {
75
+ if (part.includes(":")) {
76
+ const lastColonIndex = part.lastIndexOf(":");
77
+ const label = part.slice(0, lastColonIndex); // "desktop" or "[&:hover]"
78
+ const clsPart = part.slice(lastColonIndex + 1); // e.g. "mt1"
79
+ const classes = clsPart.split(",");
80
+
81
+ if (label === "[]") return;
82
+
83
+ if (!USAGES[filePath][label]) {
84
+ USAGES[filePath][label] = [];
83
85
  }
84
- });
85
- }
86
+ classes.forEach((cls) => {
87
+ USAGES[filePath][label].push(cls);
88
+ });
89
+ }
90
+ });
86
91
  }
87
92
  function traverseASTNode(node, visitors, stack = []) {
88
93
  if (!node || typeof node.type !== "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgecss",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "ForgeCSS turns strings into fully generated responsive CSS using a custom DSL.",
6
6
  "author": "Krasimir Tsonev",
File without changes