├── packages ├── forgecss │ ├── .gitignore │ ├── .npmignore │ ├── fx.d.ts │ ├── tsconfig.json │ ├── fx.js │ ├── lib │ │ ├── constants.js │ │ ├── forge-lang │ │ │ ├── utils.js │ │ │ ├── constants.js │ │ │ ├── Compiler.js │ │ │ └── Parser.js │ │ ├── usages.js │ │ ├── fx.js │ │ ├── inventory.js │ │ └── helpers.js │ ├── tests │ │ ├── cases │ │ │ ├── 03-media-query │ │ │ │ ├── src │ │ │ │ │ ├── styles.css │ │ │ │ │ ├── page.html │ │ │ │ │ └── another.html │ │ │ │ ├── expected.css │ │ │ │ └── test.js │ │ │ ├── 01-usages │ │ │ │ ├── src │ │ │ │ │ ├── page.html │ │ │ │ │ └── page.tsx │ │ │ │ └── test.js │ │ │ ├── 06-apply │ │ │ │ └── test.js │ │ │ ├── 02-fx │ │ │ │ └── test.js │ │ │ ├── 04-pseudo │ │ │ │ └── test.js │ │ │ ├── 07-bundleAll │ │ │ │ └── test.js │ │ │ ├── 05-arbitrary │ │ │ │ └── test.js │ │ │ └── 00-lang │ │ │ │ └── test.js │ │ ├── run.js │ │ └── helpers.js │ ├── index.d.ts │ ├── scripts │ │ └── build.js │ ├── index.fx.js │ ├── package.json │ ├── dist │ │ └── client.min.js │ ├── index.cli.js │ ├── index.browser.js │ └── index.js └── site │ ├── tsconfig.json │ ├── src │ ├── main.tsx │ ├── utils │ │ └── transformHtmlClassAttributes.ts │ ├── constants.ts │ ├── Editor.tsx │ ├── Syntax.tsx │ ├── index.css │ └── App.tsx │ ├── vite.config.ts │ ├── forgecss.config.json │ ├── public │ ├── code.svg │ ├── align-left.svg │ ├── github.svg │ ├── sliders.svg │ ├── prism.css │ ├── forgecss.svg │ ├── styles.css │ ├── input.svg │ ├── output.svg │ └── prism.js │ ├── .gitignore │ ├── eslint.config.js │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── index.html │ ├── package.json │ └── README.md ├── examples ├── react │ ├── tsconfig.json │ ├── forgecss.config.js │ ├── vite.config.ts │ ├── src │ │ ├── main.tsx │ │ ├── forgecss.css │ │ ├── index.css │ │ └── App.tsx │ ├── .gitignore │ ├── index.html │ ├── eslint.config.js │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── README.md │ └── ast.json └── vanilla │ ├── package.json │ ├── public │ ├── forgecss.css │ ├── index.html │ ├── styles.css │ └── about.html │ ├── dev.js │ └── package-lock.json ├── README.md ├── LICENSE └── .gitignore /packages/forgecss/.gitignore: -------------------------------------------------------------------------------- 1 | ast.json -------------------------------------------------------------------------------- /packages/forgecss/.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | node_modules -------------------------------------------------------------------------------- /packages/forgecss/fx.d.ts: -------------------------------------------------------------------------------- 1 | export default function fx(classes: string): string; 2 | -------------------------------------------------------------------------------- /packages/forgecss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*", "index.d.ts", "**/*.js"], 3 | } -------------------------------------------------------------------------------- /packages/forgecss/fx.js: -------------------------------------------------------------------------------- 1 | import forgeCSSExpressionTransformer from "./lib/fx.js"; 2 | 3 | export default forgeCSSExpressionTransformer; 4 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/site/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App.tsx' 3 | 4 | createRoot(document.getElementById('root')!).render( 5 | 6 | ) 7 | -------------------------------------------------------------------------------- /examples/react/forgecss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "dir": "./src", 3 | "output": "./src/forgecss.css", 4 | "breakpoints": { 5 | "desktop": "all and (min-width: 768px)" 6 | }, 7 | "minify": false 8 | } -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "dependencies": { 5 | "express": "^5.2.1" 6 | }, 7 | "scripts": { 8 | "dev": "node ./dev.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /packages/site/forgecss.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dir": "./src", 3 | "output": "./public/styles.css", 4 | "breakpoints": { 5 | "desktop": "all and (min-width: 780px)", 6 | "mobile": "all and (max-width: 779px)" 7 | }, 8 | "bundleAll": true, 9 | "minify": true 10 | } -------------------------------------------------------------------------------- /examples/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /packages/forgecss/lib/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_OPTIONS = { 2 | inventoryFiles: ["css", "less", "scss"], 3 | usageFiles: ["html", "jsx", "tsx"], 4 | usageAttributes: ["class", "className"], 5 | breakpoints: {}, 6 | verbose: true, 7 | minify: true, 8 | bundleAll: false 9 | }; 10 | -------------------------------------------------------------------------------- /packages/site/public/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/03-media-query/src/styles.css: -------------------------------------------------------------------------------- 1 | .inline { 2 | display: inline; 3 | } 4 | .mt1 { 5 | margin-top: 0.25rem; 6 | } 7 | .mt2 { 8 | margin-top: 0.5rem; 9 | } 10 | .pt2 { 11 | padding-top: 0.5rem; 12 | } 13 | .fz2 { 14 | font-size: 2rem; 15 | } 16 | .no-used { 17 | color: yellow; 18 | } 19 | .red { 20 | color: red; 21 | } -------------------------------------------------------------------------------- /packages/site/src/utils/transformHtmlClassAttributes.ts: -------------------------------------------------------------------------------- 1 | export default function transformHtmlClassAttributes(html: string, mapClass: Function) { 2 | // @ts-ignore 3 | return html.replace(/\bclass\s*=\s*(["'])(.*?)\1/gs, (m, quote, value) => { 4 | const next = mapClass(value); 5 | if (next == null) return ""; 6 | return `class=${quote}${next}${quote}`; 7 | }); 8 | } -------------------------------------------------------------------------------- /packages/site/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/03-media-query/src/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 |

11 |
12 | 13 | -------------------------------------------------------------------------------- /packages/site/public/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/03-media-query/src/another.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 |

11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ForgeCSS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ForgeCSS 2 | 3 | ForgeCSS turns strings into fully generated responsive CSS using a custom DSL. 4 | 5 | ## Client ForgeCSS Expression transformer for vanilla apps 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | ## Tests 12 | 13 | To run the tests: 14 | 15 | ``` 16 | > npm run tests 17 | > npm run tests -- --spec=02 18 | ``` 19 | 20 | in `/packages/forgetcss`. -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/01-usages/src/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | Hello World 11 |
12 |
13 |

14 | This is a paragraph. 15 |

16 |
17 | 18 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/03-media-query/expected.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 1024px) { 2 | .desktop_mt2 { 3 | margin-top: 0.5rem 4 | } 5 | .desktop_mt1 { 6 | margin-top: 0.25rem 7 | } 8 | } 9 | @media all and (max-width: 1023px) { 10 | .mobile_fz2 { 11 | font-size: 2rem 12 | } 13 | .mobile_red { 14 | color: red 15 | } 16 | } 17 | @media all and (orientation: portrait) { 18 | .portrait_pt2 { 19 | padding-top: 0.5rem 20 | } 21 | } -------------------------------------------------------------------------------- /packages/site/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vanilla/public/forgecss.css: -------------------------------------------------------------------------------- 1 | /* ForgeCSS auto-generated file */ 2 | @media all and (min-width: 768px) { 3 | .desktop_maxw600 { 4 | max-width: 600px 5 | } 6 | .desktop_p2 { 7 | padding: 2rem 8 | } 9 | .desktop_green { 10 | color: #009f00 11 | } 12 | .desktop_white-bg { 13 | background-color: #fff 14 | } 15 | } 16 | .hover_blue:hover { 17 | color: #0c5bb0; 18 | text-decoration: underline 19 | } 20 | @media all and (max-width: 768px) { 21 | .mobile_fz-sm { 22 | font-size: 0.8em 23 | } 24 | } -------------------------------------------------------------------------------- /packages/forgecss/lib/forge-lang/utils.js: -------------------------------------------------------------------------------- 1 | export function minifyCSS(css) { 2 | return ( 3 | css 4 | // remove comments 5 | .replace(/\/\*[\s\S]*?\*\//g, "") 6 | // remove whitespace around symbols 7 | .replace(/\s*([{}:;,])\s*/g, "$1") 8 | // remove trailing semicolons 9 | .replace(/;}/g, "}") 10 | // collapse multiple spaces 11 | .replace(/\s+/g, " ") 12 | // remove spaces before/after braces 13 | .replace(/\s*{\s*/g, "{") 14 | .replace(/\s*}\s*/g, "}") 15 | // trim 16 | .trim() 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/forgecss/lib/forge-lang/constants.js: -------------------------------------------------------------------------------- 1 | export const NODE_TYPE = { 2 | VARIANT: "variant", 3 | CALL: "call", 4 | TOKEN: "token" 5 | }; 6 | export const ALLOWED_PSEUDO_CLASSES = [ 7 | "hover", 8 | "active", 9 | "focus", 10 | "focus-visible", 11 | "focus-within", 12 | "disabled", 13 | "enabled", 14 | "read-only", 15 | "read-write", 16 | "checked", 17 | "indeterminate", 18 | "valid", 19 | "invalid", 20 | "required", 21 | "optional", 22 | "in-range", 23 | "out-of-range", 24 | "placeholder-shown", 25 | "autofill", 26 | "user-invalid" 27 | ]; 28 | -------------------------------------------------------------------------------- /packages/site/public/sliders.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/03-media-query/test.js: -------------------------------------------------------------------------------- 1 | import ForgeCSS from '../../../index.js'; 2 | import { getPath, expect } from '../../helpers.js'; 3 | 4 | const __dirname = '/cases/03-media-query'; 5 | 6 | export default async function test() { 7 | const forgecss = ForgeCSS({ 8 | minify: false, 9 | breakpoints: { 10 | desktop: "all and (min-width: 1024px)", 11 | mobile: "all and (max-width: 1023px)", 12 | portrait: "all and (orientation: portrait)" 13 | } 14 | }); 15 | const result = await forgecss.parseDirectory({ dir: getPath(__dirname + "/src") }); 16 | return expect.toEqualFile(result, __dirname + "/expected.css"); 17 | } -------------------------------------------------------------------------------- /examples/react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { defineConfig, globalIgnores } from 'eslint/config' 7 | 8 | export default defineConfig([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs.flat.recommended, 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /packages/site/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { defineConfig, globalIgnores } from 'eslint/config' 7 | 8 | export default defineConfig([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs.flat.recommended, 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /packages/forgecss/index.d.ts: -------------------------------------------------------------------------------- 1 | export type ForgeCSSOptions = { 2 | inventoryFiles?: string[]; 3 | usageFiles?: string[]; 4 | usageAttributes?: string[]; 5 | breakpoints?: { 6 | [key: string]: string; 7 | }; 8 | verbose?: boolean; 9 | minify?: boolean; 10 | bundleAll?: boolean; 11 | }; 12 | 13 | export type ForgeInstance = { 14 | parseDirectory: (options: { dir: string; output?: string; watch?: boolean }) => Promise; 15 | parseFile: (options: { file: string; output?: string; watch?: boolean }) => Promise; 16 | parse: (options: { css: string; html?: string; jsx?: string; output?: string; }) => Promise; 17 | }; 18 | 19 | declare function ForgeCSS(options?: ForgeCSSOptions): ForgeInstance; 20 | 21 | export default ForgeCSS; -------------------------------------------------------------------------------- /examples/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "types": ["node"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "erasableSyntaxOnly": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["vite.config.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/site/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "types": ["node"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "erasableSyntaxOnly": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["vite.config.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/06-apply/test.js: -------------------------------------------------------------------------------- 1 | import { minifyCSS, expect } from "../../helpers.js"; 2 | import ForgeCSS from "../../../index.js"; 3 | 4 | const CASES = [ 5 | { 6 | styles: ` 7 | .red { color: red } 8 | .mt1 { margin-top: 1rem } 9 | .box > div, p + p { 10 | padding: 1rem; 11 | --apply: red mt1; 12 | } 13 | `, 14 | usage: "box", 15 | expectedCSS: ".box > div,p + p{color:red;margin-top:1rem}" 16 | } 17 | ]; 18 | 19 | export default async function test() { 20 | for (let testCase of CASES) { 21 | const css = await ForgeCSS().parse({ 22 | css: testCase.styles, 23 | html: `
` 24 | }); 25 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) { 26 | return false; 27 | } 28 | } 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /examples/react/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "types": ["vite/client"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "erasableSyntaxOnly": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/site/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "types": ["vite/client"], 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "erasableSyntaxOnly": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/02-fx/test.js: -------------------------------------------------------------------------------- 1 | import fx from '../../../lib/fx.js'; 2 | import { expect } from '../../helpers.js'; 3 | 4 | const transformations = [ 5 | ["", ""], 6 | ["a b", "a b"], 7 | ["a d:x y", "a d_x y"], 8 | ["a d:x,z m:y", "a d_x d_z m_y"], 9 | ["hover:red a", "hover_red a"], 10 | ["[&:hover]:a b", "I-hover_a b"], 11 | ["[&:required:disabled]:red", "I-required-disabled_red"], 12 | ["[true]:my1 red", "my1 red"], 13 | ["[false]:my1 red", "red"], 14 | ["[.dark &]:black mt2", "dark-I_black mt2"], 15 | ["mt1 [.dark &[type='password']]:mt2", "mt1 dark-Itype-password_mt2"], 16 | ["desktop:[.dark &]:b", "desktop-dark-I_b"] 17 | ]; 18 | 19 | export default async function test() { 20 | for(let [input, expected] of transformations) { 21 | if (!expect.toBe(fx(input), expected)) { 22 | return false; 23 | } 24 | } 25 | return true; 26 | } -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/04-pseudo/test.js: -------------------------------------------------------------------------------- 1 | import { minifyCSS, expect } from "../../helpers.js"; 2 | import ForgeCSS from '../../../index.js' 3 | 4 | const CASES = [ 5 | { 6 | styles: ".red { color: red }", 7 | usage: '
', 8 | expected: ".hover_red:hover{color:red}" 9 | }, 10 | { 11 | styles: ".red { color: red }.mt2 { margin-top: 2rem }", 12 | usage: '
', 13 | expected: ".focus_red:focus{color:red}.focus_mt2:focus{margin-top:2rem}.active_mt2:active{margin-top:2rem}" 14 | } 15 | ]; 16 | 17 | export default async function test() { 18 | for (let testCase of CASES) { 19 | const css = await ForgeCSS().parse({ css: testCase.styles, html: testCase.usage }); 20 | if (!expect.toBe(minifyCSS(css), testCase.expected)) { 21 | return false; 22 | } 23 | } 24 | return true; 25 | } -------------------------------------------------------------------------------- /packages/forgecss/scripts/build.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import esbuild from "esbuild"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | const minify = true; 8 | 9 | (async function () { 10 | await esbuild.build({ 11 | entryPoints: [path.join(__dirname, "..", "index.fx.js")], 12 | bundle: true, 13 | minify, 14 | outfile: path.join(__dirname, "..", "dist", "client.min.js"), 15 | platform: "browser", 16 | sourcemap: false, 17 | plugins: [] 18 | }); 19 | await esbuild.build({ 20 | entryPoints: [path.join(__dirname, "..", "index.browser.js")], 21 | bundle: true, 22 | minify, 23 | outfile: path.join(__dirname, "..", "dist", "standalone.min.js"), 24 | platform: "browser", 25 | sourcemap: false, 26 | plugins: [] 27 | }); 28 | })(); 29 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/01-usages/src/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fx } from 'forgecss/fx'; 3 | 4 | export default function App({ className }: { className?: string }) { 5 | const foo = 'bar'; 6 | const flagA = true; 7 | const flagB = false; 8 | 9 | return ( 10 |
11 |

Hello world!

12 |

Something else

13 |

No usage of fx so no pick up

14 |

15 | 16 |

17 |

18 |

19 |

20 |
21 | ); 22 | } -------------------------------------------------------------------------------- /packages/forgecss/index.fx.js: -------------------------------------------------------------------------------- 1 | import fxFn from "./lib/fx.js"; 2 | 3 | function fxAll(root) { 4 | var rootNode = root || document; 5 | var nodes = rootNode.querySelectorAll('[class]'); 6 | 7 | for (var i = 0; i < nodes.length; i++) { 8 | var el = nodes[i]; 9 | var original = el.getAttribute('class'); 10 | if (!original) continue; 11 | 12 | var transformed = fxFn(original); 13 | 14 | if (typeof transformed === 'string' && transformed !== original) { 15 | el.setAttribute('class', transformed); 16 | } 17 | } 18 | } 19 | 20 | window.fx = function(str) { 21 | if (str) { 22 | return fxFn(str); 23 | } 24 | return fxAll(); 25 | }; 26 | 27 | if (document.readyState !== 'loading') { 28 | fxAll(); 29 | } else { 30 | document.addEventListener('DOMContentLoaded', function () { 31 | fxAll(); 32 | }); 33 | } 34 | window.addEventListener('load', function () { 35 | fxAll(); 36 | }); -------------------------------------------------------------------------------- /examples/react/src/forgecss.css: -------------------------------------------------------------------------------- 1 | /* ForgeCSS auto-generated file */ 2 | @media all and (min-width: 768px) { 3 | .desktop_w400 { 4 | width: 400px 5 | } 6 | .desktop_p2 { 7 | padding: 2rem 8 | } 9 | .desktop_flex-row { 10 | display: flex; 11 | flex-direction: row 12 | } 13 | .desktop_align-center { 14 | align-items: center 15 | } 16 | .desktop_autow { 17 | width: auto 18 | } 19 | } 20 | .light .light-I_surface-light { 21 | background-color: var(--surface-light) 22 | } 23 | .light .light-I_text-light { 24 | color: var(--text-light) 25 | } 26 | .disabled_op05:disabled { 27 | opacity: 0.5 28 | } 29 | .hover_primary2-bg:hover { 30 | background-color: var(--primary-color2) 31 | } 32 | .I-disabled_op05:disabled { 33 | opacity: 0.5 34 | } 35 | .I-disabled-hover_primary-bg:disabled:hover { 36 | background-color: var(--primary-color) 37 | } -------------------------------------------------------------------------------- /examples/vanilla/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ForgeCSS 7 | 8 | 9 | 10 | 11 |
12 |
13 | About the project 14 |
15 |

Hey

16 |

ForgeCSS parses className strings as a small DSL and generates responsive CSS based on your own utility classes. It gives you Tailwind-like breakpoint expressiveness without adopting a new styling framework.

17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/07-bundleAll/test.js: -------------------------------------------------------------------------------- 1 | import { minifyCSS, expect } from "../../helpers.js"; 2 | import ForgeCSS from "../../../index.js"; 3 | 4 | const CASES = [ 5 | { 6 | styles: ` 7 | .red { color: red } 8 | .mt1 { margin-top: 1rem } 9 | `, 10 | usage: "red desktop:mt1", 11 | expectedCSS: ".red{color:red}.mt1{margin-top:1rem}@media @media (min-width:750px){.desktop_mt1{margin-top:1rem}}" 12 | } 13 | ]; 14 | 15 | export default async function test() { 16 | for (let testCase of CASES) { 17 | const css = await ForgeCSS({ 18 | breakpoints: { 19 | desktop: "@media (min-width: 750px)" 20 | }, 21 | bundleAll: true 22 | }).parse({ 23 | css: testCase.styles, 24 | html: `
` 25 | }); 26 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) { 27 | return false; 28 | } 29 | } 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "concurrently \"vite\" \"node ../../packages/forgecss/index.cli.js -w -v\"", 8 | "build": "forgecss --verbose && tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "concurrently": "^9.2.1", 14 | "forgecss": "^0.1.7", 15 | "react": "^19.2.0", 16 | "react-dom": "^19.2.0" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.39.1", 20 | "@types/node": "^24.10.1", 21 | "@types/react": "^19.2.5", 22 | "@types/react-dom": "^19.2.3", 23 | "@vitejs/plugin-react": "^5.1.1", 24 | "eslint": "^9.39.1", 25 | "eslint-plugin-react-hooks": "^7.0.1", 26 | "eslint-plugin-react-refresh": "^0.4.24", 27 | "globals": "^16.5.0", 28 | "typescript": "~5.9.3", 29 | "typescript-eslint": "^8.46.4", 30 | "vite": "^7.2.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/vanilla/public/styles.css: -------------------------------------------------------------------------------- 1 | *, :after, :before { 2 | box-sizing: border-box; 3 | } 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | font-family: Arial, sans-serif; 8 | font-size: 20px; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | main { 13 | height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | } 19 | .grey-bg { 20 | background-color: #d1d1d1; 21 | } 22 | .white-bg { 23 | background-color: #fff; 24 | } 25 | .maxw300 { 26 | max-width: 300px; 27 | } 28 | .maxw600 { 29 | max-width: 600px; 30 | } 31 | .maxw900 { 32 | max-width: 900px; 33 | } 34 | .p1 { 35 | padding: 1rem; 36 | } 37 | .p2 { 38 | padding: 2rem; 39 | } 40 | .br1 { 41 | border-radius: 1rem; 42 | } 43 | .fz-sm { 44 | font-size: 0.8em; 45 | } 46 | .blue { 47 | color: #0c5bb0; 48 | text-decoration: underline; 49 | } 50 | .green { 51 | color: #009f00; 52 | } 53 | .transition-bg-color { 54 | transition: background-color 800ms; 55 | } -------------------------------------------------------------------------------- /packages/forgecss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forgecss", 3 | "version": "0.14.0", 4 | "type": "module", 5 | "description": "ForgeCSS turns strings into fully generated responsive CSS using a custom DSL.", 6 | "author": "Krasimir Tsonev", 7 | "main": "index.js", 8 | "scripts": { 9 | "build": "node ./scripts/build.js", 10 | "test": "node ./tests/run.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:krasimir/ForgeCSS.git" 15 | }, 16 | "license": "MIT", 17 | "keywords": [ 18 | "css", 19 | "dsl", 20 | "generator", 21 | "processor", 22 | "responsive", 23 | "media queries" 24 | ], 25 | "dependencies": { 26 | "@swc/cli": "^0.7.7", 27 | "@swc/core": "1.12.1", 28 | "chokidar": "^4.0.3", 29 | "commander": "^14.0.2", 30 | "postcss": "^8.5.6", 31 | "postcss-safe-parser": "^7.0.1" 32 | }, 33 | "devDependencies": { 34 | "esbuild": "^0.27.1" 35 | }, 36 | "bin": { 37 | "forgecss": "./index.cli.js" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ForgeCSS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/vanilla/dev.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "path"; 3 | import express from "express"; 4 | import { fileURLToPath } from "url"; 5 | import ForgeCSS from "../../packages/forgecss/index.js"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | const fxCode = fs.readFileSync(path.join(__dirname, "../../packages/forgecss/dist/client.min.js"), "utf-8"); 11 | 12 | const PORT = 5173; 13 | const app = express(); 14 | 15 | ForgeCSS({ 16 | breakpoints: { 17 | desktop: "all and (min-width: 768px)", 18 | mobile: "all and (max-width: 768px)" 19 | }, 20 | minify: false, 21 | }).parseDirectory({ 22 | dir: path.join(__dirname, "public"), 23 | output: path.join(__dirname, "public/forgecss.css"), 24 | watch: true 25 | }); 26 | 27 | app.get("/fx.min.js", (req, res) => { 28 | res.type("application/javascript"); 29 | res.send(fxCode); 30 | }); 31 | app.use(express.static("public")); 32 | 33 | app.listen(PORT, () => { 34 | console.log("Server is running on http://localhost:" + PORT); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Krasimir Tsonev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "get-standalone": "cp ../forgecss/dist/standalone.min.js ./public/standalone.min.js", 8 | "dev": "npm run get-standalone && concurrently \"vite\" \"forgecss -w -v\"", 9 | "build": "npm run get-standalone && forgecss --verbose && tsc -b && vite build", 10 | "lint": "eslint .", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "concurrently": "^9.2.1", 15 | "forgecss": "^0.14.0", 16 | "monaco-editor": "^0.32.1", 17 | "react": "^19.2.0", 18 | "react-dom": "^19.2.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.39.1", 22 | "@types/node": "^24.10.1", 23 | "@types/react": "^19.2.5", 24 | "@types/react-dom": "^19.2.3", 25 | "@vitejs/plugin-react": "^5.1.1", 26 | "eslint": "^9.39.1", 27 | "eslint-plugin-react-hooks": "^7.0.1", 28 | "eslint-plugin-react-refresh": "^0.4.24", 29 | "globals": "^16.5.0", 30 | "typescript": "~5.9.3", 31 | "typescript-eslint": "^8.46.4", 32 | "vite": "^7.2.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/forgecss/dist/client.min.js: -------------------------------------------------------------------------------- 1 | (()=>{function c(t){let n=t.lastIndexOf(":");if(n===-1)return[null,t];let e=t.slice(0,n),o=t.slice(n+1);return[e,o]}function s(t){let n=[],e="",o=0,l=null;for(let r=0;r0){if(l){e+=i,i===l&&t[r-1]!=="\\"&&(l=null);continue}else if(i==="'"||i==='"'){l=i,e+=i;continue}}if(i==="["){o++,e+=i;continue}if(i==="]"&&o>0){o--,e+=i;continue}if(o===0&&/\s/.test(i)){for(e&&n.push(e),e="";r+1{let[e,o]=c(n);return!e||e==="[true]"?o:e==="[false]"?!1:(e=a(e),o.split(",").map(l=>`${e}_${l}`).join(" "))}).filter(Boolean).join(" ")}function a(t){let n=t.trim();return n=n.replace(/[&]/g,"I"),n=n.replace(/[:| =]/g,"-"),n=n.replace(/[^a-zA-Z0-9_-]/g,""),n}function f(t){for(var n=t||document,e=n.querySelectorAll("[class]"),o=0;o 5 |

Hey world!

6 |

7 | I'm ForgeCSS. 8 |

9 | `, 10 | selected: true, 11 | type: "html" 12 | }, 13 | { 14 | filename: "styles.css", 15 | content: `.mt1 { margin-top: 1rem; } 16 | .mt2 { margin-top: 2rem; } 17 | .white { color: #fff; } 18 | .black-bg { background-color: #000; } 19 | .red { color: #9f0000; }`, 20 | selected: false, 21 | type: "css" 22 | }, 23 | { 24 | filename: "forgecss.config.json", 25 | content: `{ 26 | "breakpoints": { 27 | "d": "all and (min-width: 768px)" 28 | }, 29 | "bundleAll": true 30 | }`, 31 | selected: false, 32 | type: "javascript" 33 | } 34 | ]; 35 | export const ACTUAL_HTML_FILE = { 36 | filename: "page.html", 37 | content: "", 38 | selected: true, 39 | type: "html" 40 | }; 41 | export const TOTAL_CSS_FILE = { 42 | filename: "styles.css", 43 | content: "", 44 | selected: false, 45 | type: "css" 46 | }; 47 | export const DEFAULT_OUTPUT_FILES = [ACTUAL_HTML_FILE, TOTAL_CSS_FILE]; -------------------------------------------------------------------------------- /packages/forgecss/lib/usages.js: -------------------------------------------------------------------------------- 1 | let USAGES = {}; 2 | 3 | export async function findUsages(filePath, content, JSXParser) { 4 | try { 5 | if (USAGES[filePath]) { 6 | // already processed 7 | return; 8 | } 9 | USAGES[filePath] = []; 10 | const extension = filePath.split(".").pop().toLowerCase(); 11 | 12 | // HTML 13 | if (extension === "html") { 14 | extractClassNamesFromHTML(content).forEach((cls) => { 15 | USAGES[filePath].push(cls); 16 | }); 17 | return; 18 | } 19 | if (JSXParser && (extension === "jsx" || extension === "tsx")) { 20 | await JSXParser(content, USAGES, filePath); 21 | return; 22 | } 23 | } catch (err) { 24 | console.error(`forgecss: error processing file ${filePath.replace(process.cwd(), "")}`, err); 25 | } 26 | } 27 | export function invalidateUsageCache(filePath) { 28 | if (!filePath) { 29 | USAGES = {}; 30 | return; 31 | } 32 | if (USAGES[filePath]) { 33 | delete USAGES[filePath]; 34 | } 35 | } 36 | export function getUsages() { 37 | return USAGES; 38 | } 39 | 40 | function extractClassNamesFromHTML(html) { 41 | const result = []; 42 | const classAttrRE = /\bclass\s*=\s*(["'])(.*?)\1/gis; 43 | 44 | let match; 45 | while ((match = classAttrRE.exec(html))) { 46 | result.push(match[2].trim()); 47 | } 48 | 49 | return result; 50 | } -------------------------------------------------------------------------------- /examples/react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/public/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.30.0 2 | https://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+jsx+tsx+typescript */ 3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/01-usages/test.js: -------------------------------------------------------------------------------- 1 | import { JSXParser, readFileContent } from "../../../lib/helpers.js"; 2 | import { invalidateInventory } from "../../../lib/inventory.js"; 3 | import { findUsages, getUsages, invalidateUsageCache } from "../../../lib/usages.js"; 4 | import { getPath, expect } from "../../helpers.js"; 5 | 6 | export default async function test() { 7 | const cases = [ 8 | { 9 | file: getPath("/cases/01-usages/src/page.html"), 10 | expected: { 11 | [getPath("/cases/01-usages/src/page.html")]: [ 12 | "mt1 desktop:red", 13 | "fz3 mx-l [.dark &:hover]:red", 14 | "vibe" 15 | ] 16 | } 17 | }, 18 | { 19 | file: getPath("/cases/01-usages/src/page.tsx"), 20 | expected: { 21 | [getPath("/cases/01-usages/src/page.tsx")]: [ 22 | "a desktop:b", 23 | "c mobile:d desktop:b2 e", 24 | "a []:b []:c", 25 | "[&:hover]:a", 26 | "a [.dark &]:b c", 27 | "a [.dark desktop:b]:c d", 28 | "a [.dark &:has(.desc)]:c d", 29 | "a [.dark &[type='password']]:c d" 30 | ] 31 | } 32 | } 33 | ]; 34 | for (let i=0; i 2 | 3 | 4 | 5 | 6 | ForgeCSS 7 | 8 | 9 | 10 | 11 |
12 |
13 | Back home 14 |
15 |

ForgeCSS introduces ForgeLang, a lightweight DSL embedded directly into your React className strings. It lets you express responsive design using intuitive prefixes like desktop:mt-4 or mobile:text-sm, then compiles those tokens into real CSS. Instead of reinventing a utility framework, ForgeCSS builds on top of the styles you already have—reading your existing CSS utilities, extracting their declarations, and generating fully scoped media-query variants automatically. This means your design system stays in control while ForgeCSS handles the heavy lifting of responsive expansion.

16 |

The project was created for teams who love utility classes but want more expressive breakpoint logic without adopting a full Tailwind pipeline. By treating className as a parseable language rather than a plain string, ForgeCSS enables custom syntax, prefix extensions, and future transformations that can evolve with your codebase. It slots into any React project with minimal setup and produces predictable, deduplicated CSS output that mirrors your real styles. Think of it as a tiny compiler for responsive utilities—simple to adopt, endlessly flexible, and built for modern UI development.

17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/forgecss/lib/fx.js: -------------------------------------------------------------------------------- 1 | function splitClassName(label) { 2 | const lastColonIndex = label.lastIndexOf(":"); 3 | if (lastColonIndex === -1) { 4 | return [null, label]; 5 | } 6 | const prefix = label.slice(0, lastColonIndex); 7 | const rest = label.slice(lastColonIndex + 1); 8 | return [prefix, rest]; 9 | } 10 | function parseClass(str) { 11 | const out = []; 12 | let buf = ""; 13 | 14 | let depth = 0; 15 | let quote = null; 16 | for (let i = 0; i < str.length; i++) { 17 | const ch = str[i]; 18 | if (depth > 0) { 19 | if (quote) { 20 | buf += ch; 21 | if (ch === quote && str[i - 1] !== "\\") quote = null; 22 | continue; 23 | } else if (ch === "'" || ch === '"') { 24 | quote = ch; 25 | buf += ch; 26 | continue; 27 | } 28 | } 29 | if (ch === "[") { 30 | depth++; 31 | buf += ch; 32 | continue; 33 | } 34 | if (ch === "]" && depth > 0) { 35 | depth--; 36 | buf += ch; 37 | continue; 38 | } 39 | if (depth === 0 && /\s/.test(ch)) { 40 | if (buf) out.push(buf); 41 | buf = ""; 42 | while (i + 1 < str.length && /\s/.test(str[i + 1])) i++; 43 | continue; 44 | } 45 | buf += ch; 46 | } 47 | 48 | if (buf) out.push(buf); 49 | return out; 50 | } 51 | export default function fx(classes) { 52 | return parseClass(classes) 53 | .map((className) => { 54 | let [label, rest] = splitClassName(className); 55 | if (!label || label === "[true]") return rest; 56 | if (label === "[false]") return false; 57 | label = normalizeLabel(label); 58 | return rest 59 | .split(",") 60 | .map((cls) => `${label}_${cls}`) 61 | .join(" "); 62 | }) 63 | .filter(Boolean) 64 | .join(" "); 65 | } 66 | export function normalizeLabel(label) { 67 | let normalized = label.trim(); 68 | normalized = normalized.replace(/[&]/g, "I"); 69 | normalized = normalized.replace(/[:| =]/g, "-"); 70 | normalized = normalized.replace(/[^a-zA-Z0-9_-]/g, ""); 71 | return normalized; 72 | } -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/05-arbitrary/test.js: -------------------------------------------------------------------------------- 1 | import { minifyCSS, expect } from "../../helpers.js"; 2 | import fx from "../../../lib/fx.js"; 3 | import ForgeCSS from "../../../index.js"; 4 | 5 | const CASES = [ 6 | { 7 | styles: ` 8 | .red { color: red } 9 | `, 10 | usage: "[&:hover]:red", 11 | expectedClass: "I-hover_red", 12 | expectedCSS: ".I-hover_red:hover{color:red}", 13 | type: "html" 14 | }, 15 | { 16 | styles: ` 17 | .red { color: red } 18 | .fz2 { font-size: 2rem } 19 | .mt2 { margin-top: 2rem } 20 | `, 21 | usage: `[&:hover]:red,fz2 [&:hover]:mt2`, 22 | expectedClass: "I-hover_red I-hover_fz2 I-hover_mt2", 23 | expectedCSS: 24 | ".I-hover_red:hover{color:red}.I-hover_fz2:hover{font-size:2rem}.I-hover_mt2:hover{margin-top:2rem}", 25 | type: "html" 26 | }, 27 | { 28 | styles: ` 29 | .red { color: red } 30 | .fz2 { font-size: 2rem } 31 | `, 32 | usage: `[&:required:disabled]:red,fz2`, 33 | expectedClass: "I-required-disabled_red I-required-disabled_fz2", 34 | expectedCSS: 35 | ".I-required-disabled_red:required:disabled{color:red}.I-required-disabled_fz2:required:disabled{font-size:2rem}", 36 | type: "html" 37 | }, 38 | { 39 | styles: ` 40 | .red { color: red } 41 | `, 42 | usage: `[.dark &]:red`, 43 | expectedClass: "dark-I_red", 44 | expectedCSS: ".dark .dark-I_red{color:red}", 45 | type: "html" 46 | }, 47 | { 48 | styles: ` 49 | .red { color: red } 50 | `, 51 | usage: "[true]:red", 52 | expectedClass: "red", 53 | expectedCSS: "", 54 | type: "jsx" 55 | } 56 | ]; 57 | 58 | export default async function test() { 59 | for (let testCase of CASES) { 60 | const css = await ForgeCSS().parse({ 61 | css: testCase.styles, 62 | [testCase.type || "html"]: 63 | testCase.type === "html" 64 | ? `
` 65 | : `function Component() { return
}` 66 | }); 67 | if (!expect.toBe(fx(testCase.usage), testCase.expectedClass)) { 68 | return false; 69 | } 70 | if (!expect.toBe(minifyCSS(css), testCase.expectedCSS)) { 71 | return false; 72 | } 73 | } 74 | return true; 75 | } -------------------------------------------------------------------------------- /packages/forgecss/lib/inventory.js: -------------------------------------------------------------------------------- 1 | import postcss from "postcss"; 2 | import safeParser from "postcss-safe-parser"; 3 | 4 | let INVENTORY = {}; 5 | 6 | export function extractStyles(filePath, content) { 7 | INVENTORY[filePath] = postcss.parse(content, { parser: safeParser }); 8 | } 9 | export function getStylesByClassName(selector) { 10 | const decls = []; 11 | Object.keys(INVENTORY).forEach((filePath) => { 12 | INVENTORY[filePath].walkRules((rule) => { 13 | if (rule.selectors && rule.selectors.includes(`.${selector}`)) { 14 | rule.walkDecls((d) => { 15 | decls.push({ prop: d.prop, value: d.value, important: d.important }); 16 | }); 17 | } 18 | }); 19 | }); 20 | if (decls.length === 0) { 21 | console.warn(`forgecss: no styles found for class "${selector}".`); 22 | } 23 | return decls; 24 | } 25 | export function invalidateInventory(filePath) { 26 | if (!filePath) { 27 | INVENTORY = {}; 28 | return; 29 | } 30 | if (INVENTORY[filePath]) { 31 | delete INVENTORY[filePath]; 32 | } 33 | } 34 | export function resolveApplys() { 35 | let resolvedApplies; 36 | Object.keys(INVENTORY).forEach((filePath) => { 37 | INVENTORY[filePath].walkRules((rule) => { 38 | rule.walkDecls((d) => { 39 | if (d.prop === "--apply") { 40 | const classesToApply = d.value 41 | .split(" ") 42 | .map((c) => c.trim()) 43 | .filter(Boolean); 44 | const newRule = postcss.rule({ selector: rule.selector }); 45 | classesToApply.forEach((className) => { 46 | const styles = getStylesByClassName(className); 47 | styles.forEach((style) => { 48 | newRule.append({ 49 | prop: style.prop, 50 | value: style.value, 51 | important: style.important 52 | }); 53 | }); 54 | }); 55 | if (!resolvedApplies) { 56 | resolvedApplies = postcss.root(); 57 | } 58 | resolvedApplies.append(newRule); 59 | } 60 | }); 61 | }); 62 | }); 63 | return resolvedApplies; 64 | } 65 | export function getInventory() { 66 | return INVENTORY; 67 | } 68 | export function getAllCSS() { 69 | let combined = ""; 70 | Object.keys(INVENTORY).forEach((filePath) => { 71 | combined += INVENTORY[filePath].toString() + "\n"; 72 | }); 73 | return combined; 74 | } -------------------------------------------------------------------------------- /packages/forgecss/tests/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | export function root() { 9 | return __dirname; 10 | } 11 | export function getPath(p) { 12 | return path.join(__dirname, p); 13 | } 14 | export const expect = { 15 | toBe(value, expected) { 16 | const result = value === expected; 17 | if (!result) { 18 | console.error("Expected:\n", expected); 19 | console.error("Received:\n", value); 20 | } 21 | return result; 22 | }, 23 | toEqualFile(str, file) { 24 | const expected = fs.readFileSync(getPath(file), "utf-8"); 25 | const result = str === expected; 26 | if (!result) { 27 | console.error("\nExpected:\n", expected); 28 | console.error("\nReceived:\n", str); 29 | } 30 | return result; 31 | }, 32 | deepEqual(actual, expected) { 33 | function deepEqual(a, b) { 34 | if (a === b) return true; 35 | 36 | if (a === null || b === null) return a === b; 37 | 38 | if (Array.isArray(a) && Array.isArray(b)) { 39 | if (a.length !== b.length) return false; 40 | for (let i = 0; i < a.length; i++) { 41 | if (!deepEqual(a[i], b[i])) return false; 42 | } 43 | return true; 44 | } 45 | 46 | if (Array.isArray(a) || Array.isArray(b)) return false; 47 | 48 | if (typeof a === "object" && typeof b === "object") { 49 | const keysA = Object.keys(a); 50 | const keysB = Object.keys(b); 51 | if (keysA.length !== keysB.length) return false; 52 | 53 | for (const key of keysA) { 54 | if (!Object.prototype.hasOwnProperty.call(b, key)) return false; 55 | if (!deepEqual(a[key], b[key])) return false; 56 | } 57 | return true; 58 | } 59 | 60 | return false; 61 | } 62 | const result = deepEqual(actual, expected); 63 | if (!result) { 64 | console.error("\nExpected:\n", JSON.stringify(expected, null, 2)); 65 | console.error("\nActual:\n", JSON.stringify(actual, null, 2)); 66 | } 67 | return result; 68 | } 69 | }; 70 | export function minifyCSS(css = '') { 71 | return css 72 | .replace(/\/\*[^]*?\*\//g, "") // remove comments 73 | .replace(/\s+/g, " ") // collapse spaces 74 | .replace(/\s*([{}:;,])\s*/g, "$1") // trim syntax whitespace 75 | .trim(); 76 | 77 | } -------------------------------------------------------------------------------- /examples/react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #4CAF50; 3 | --primary-color2: #39e63f; 4 | --secondary-color: #8bcbe9; 5 | --background: #292929; 6 | --surface: #525252; 7 | --surface-light: #b9b9b9; 8 | --text: #f0f0f0; 9 | --text-light: #000; 10 | --error: #dd6c6c; 11 | } 12 | * { 13 | box-sizing: border-box; 14 | } 15 | body { 16 | background: var(--background); 17 | color: var(--text); 18 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 19 | font-size: 20px; 20 | margin: 0; 21 | padding: 0; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | p { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | main { 30 | justify-content: center; 31 | align-items: center; 32 | height: 100vh; 33 | } 34 | fieldset { 35 | min-width: 100%; 36 | filter: drop-shadow(0 0 0.75rem black); 37 | background: #393939 38 | } 39 | input { 40 | padding: 0.5rem; 41 | border-radius: 6px; 42 | border: none; 43 | font-size: 1rem; 44 | } 45 | button { 46 | padding: 0.5rem 1rem; 47 | border-radius: 6px; 48 | border: none; 49 | background-color: var(--primary-color); 50 | color: white; 51 | cursor: pointer; 52 | font-size: 1rem; 53 | } 54 | fieldset { 55 | border-radius: 6px; 56 | display: flex; 57 | flex-direction: column; 58 | gap: 1rem; 59 | } 60 | .no-border { border: none; } 61 | .primary { color: var(--primary-color); } 62 | .primary-bg { background-color: var(--primary-color); } 63 | .primary2 { color: var(--primary-color2); } 64 | .primary2-bg { background-color: var(--primary-color2); } 65 | .secondary { color: var(--secondary-color); } 66 | .secondary-bg { background-color: var(--secondary-color); } 67 | .surface { background-color: var(--surface); } 68 | .surface-light { background-color: var(--surface-light); } 69 | .text-light { color: var(--text-light); } 70 | .error { color: var(--error); } 71 | .error-bg { background-color: var(--error); } 72 | .error-border { border: 2px solid var(--error); } 73 | .success-border { border: 2px solid var(--primary-color); } 74 | 75 | .block { display: block; } 76 | .p1 { padding: 1rem; } 77 | .p2 { padding: 2rem; } 78 | .mt1 { margin-top: 1rem; } 79 | .mt05 { margin-top: 0.5rem; } 80 | .flex { display: flex; } 81 | .grid { display: grid; } 82 | .flex-col { display: flex; flex-direction: column; } 83 | .flex-row { display: flex; flex-direction: row; } 84 | .space-between { justify-content: space-between; } 85 | .align-center { align-items: center; } 86 | .align-start { align-items: start; } 87 | .gap1 { gap: 1rem; } 88 | .fullw { width: 100%; } 89 | .autow { width: auto; } 90 | .w400 { width: 400px; } 91 | .op05 { opacity: 0.5; } -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## React Compiler 11 | 12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 | 14 | ## Expanding the ESLint configuration 15 | 16 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 17 | 18 | ```js 19 | export default defineConfig([ 20 | globalIgnores(['dist']), 21 | { 22 | files: ['**/*.{ts,tsx}'], 23 | extends: [ 24 | // Other configs... 25 | 26 | // Remove tseslint.configs.recommended and replace with this 27 | tseslint.configs.recommendedTypeChecked, 28 | // Alternatively, use this for stricter rules 29 | tseslint.configs.strictTypeChecked, 30 | // Optionally, add this for stylistic rules 31 | tseslint.configs.stylisticTypeChecked, 32 | 33 | // Other configs... 34 | ], 35 | languageOptions: { 36 | parserOptions: { 37 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 38 | tsconfigRootDir: import.meta.dirname, 39 | }, 40 | // other options... 41 | }, 42 | }, 43 | ]) 44 | ``` 45 | 46 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 47 | 48 | ```js 49 | // eslint.config.js 50 | import reactX from 'eslint-plugin-react-x' 51 | import reactDom from 'eslint-plugin-react-dom' 52 | 53 | export default defineConfig([ 54 | globalIgnores(['dist']), 55 | { 56 | files: ['**/*.{ts,tsx}'], 57 | extends: [ 58 | // Other configs... 59 | // Enable lint rules for React 60 | reactX.configs['recommended-typescript'], 61 | // Enable lint rules for React DOM 62 | reactDom.configs.recommended, 63 | ], 64 | languageOptions: { 65 | parserOptions: { 66 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 67 | tsconfigRootDir: import.meta.dirname, 68 | }, 69 | // other options... 70 | }, 71 | }, 72 | ]) 73 | ``` 74 | -------------------------------------------------------------------------------- /packages/site/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## React Compiler 11 | 12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 | 14 | ## Expanding the ESLint configuration 15 | 16 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 17 | 18 | ```js 19 | export default defineConfig([ 20 | globalIgnores(['dist']), 21 | { 22 | files: ['**/*.{ts,tsx}'], 23 | extends: [ 24 | // Other configs... 25 | 26 | // Remove tseslint.configs.recommended and replace with this 27 | tseslint.configs.recommendedTypeChecked, 28 | // Alternatively, use this for stricter rules 29 | tseslint.configs.strictTypeChecked, 30 | // Optionally, add this for stylistic rules 31 | tseslint.configs.stylisticTypeChecked, 32 | 33 | // Other configs... 34 | ], 35 | languageOptions: { 36 | parserOptions: { 37 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 38 | tsconfigRootDir: import.meta.dirname, 39 | }, 40 | // other options... 41 | }, 42 | }, 43 | ]) 44 | ``` 45 | 46 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 47 | 48 | ```js 49 | // eslint.config.js 50 | import reactX from 'eslint-plugin-react-x' 51 | import reactDom from 'eslint-plugin-react-dom' 52 | 53 | export default defineConfig([ 54 | globalIgnores(['dist']), 55 | { 56 | files: ['**/*.{ts,tsx}'], 57 | extends: [ 58 | // Other configs... 59 | // Enable lint rules for React 60 | reactX.configs['recommended-typescript'], 61 | // Enable lint rules for React DOM 62 | reactDom.configs.recommended, 63 | ], 64 | languageOptions: { 65 | parserOptions: { 66 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 67 | tsconfigRootDir: import.meta.dirname, 68 | }, 69 | // other options... 70 | }, 71 | }, 72 | ]) 73 | ``` 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variable files 69 | .env 70 | .env.* 71 | !.env.example 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | .parcel-cache 76 | 77 | # Next.js build output 78 | .next 79 | out 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and not Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # vuepress v2.x temp and cache directory 94 | .temp 95 | .cache 96 | 97 | # Sveltekit cache directory 98 | .svelte-kit/ 99 | 100 | # vitepress build output 101 | **/.vitepress/dist 102 | 103 | # vitepress cache directory 104 | **/.vitepress/cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # Firebase cache directory 119 | .firebase/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v3 128 | .pnp.* 129 | .yarn/* 130 | !.yarn/patches 131 | !.yarn/plugins 132 | !.yarn/releases 133 | !.yarn/sdks 134 | !.yarn/versions 135 | 136 | # Vite logs files 137 | vite.config.js.timestamp-* 138 | vite.config.ts.timestamp-* 139 | -------------------------------------------------------------------------------- /packages/forgecss/lib/helpers.js: -------------------------------------------------------------------------------- 1 | import swc from "@swc/core"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | 5 | const FUNC_NAME = "fx"; 6 | const { parse } = swc; 7 | 8 | export async function getAllFiles(dir, matchFiles) { 9 | const result = []; 10 | const stack = [dir]; 11 | 12 | while (stack.length > 0) { 13 | const currentDir = stack.pop(); 14 | 15 | let dirHandle; 16 | try { 17 | dirHandle = await fs.opendir(currentDir); 18 | } catch (err) { 19 | throw err; 20 | } 21 | 22 | for await (const entry of dirHandle) { 23 | const fullPath = path.join(currentDir, entry.name); 24 | 25 | if (entry.isDirectory()) { 26 | stack.push(fullPath); 27 | } else if (matchFiles.includes(fullPath.split(".").pop()?.toLowerCase())) { 28 | result.push(fullPath); 29 | } 30 | } 31 | } 32 | 33 | return result; 34 | } 35 | export function readFileContent(filePath) { 36 | return fs.readFile(filePath, "utf-8"); 37 | } 38 | export async function JSXParser(content, USAGES, filePath) { 39 | const ast = await parse(content, { 40 | syntax: "typescript", 41 | tsx: true, 42 | decorators: false 43 | }); 44 | function traverseASTNode(node, visitors, stack = []) { 45 | if (!node || typeof node.type !== "string") { 46 | return; 47 | } 48 | 49 | const visitor = visitors[node.type]; 50 | if (visitor) { 51 | visitor(node, stack); 52 | } 53 | 54 | for (const key in node) { 55 | if (!node.hasOwnProperty(key)) continue; 56 | 57 | const child = node[key]; 58 | 59 | if (Array.isArray(child)) { 60 | child.forEach((c) => { 61 | if (c) { 62 | if (typeof c.type === "string") { 63 | traverseASTNode(c, visitors, [node].concat(stack)); 64 | } else if (c?.expression && typeof c.expression.type === "string") { 65 | traverseASTNode(c.expression, visitors, [node].concat(stack)); 66 | } else if (c?.callee && typeof c.callee.type === "string") { 67 | traverseASTNode(c.callee, visitors, [node].concat(stack)); 68 | } else if (c?.left && typeof c.left.type === "string") { 69 | traverseASTNode(c.left, visitors, [node].concat(stack)); 70 | } else if (c?.right && typeof c.right.type === "string") { 71 | traverseASTNode(c.right, visitors, [node].concat(stack)); 72 | } 73 | } 74 | }); 75 | } else if (child && typeof child.type === "string") { 76 | traverseASTNode(child, visitors, [node].concat(stack)); 77 | } 78 | } 79 | } 80 | 81 | traverseASTNode(ast, { 82 | JSXExpressionContainer(node) { 83 | if (node?.expression?.callee?.value === FUNC_NAME && node?.expression?.arguments) { 84 | if (node?.expression?.arguments[0]) { 85 | const arg = node.expression.arguments[0]; 86 | let value = arg?.expression.value; 87 | if (arg.expression.type === "TemplateLiteral") { 88 | let quasis = arg.expression.quasis.map((elem) => elem?.cooked || ""); 89 | value = quasis.join(""); 90 | } 91 | USAGES[filePath].push(value); 92 | } 93 | } 94 | } 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /packages/forgecss/index.cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs'; 4 | import path from "node:path"; 5 | import { pathToFileURL } from "node:url"; 6 | import { program } from "commander"; 7 | import chokidar from "chokidar"; 8 | 9 | import ForgeCSS from './index.js'; 10 | 11 | const POSSIBLE_CONFIG_FILES = [ 12 | process.cwd() + "/forgecss.config.json", 13 | process.cwd() + "/forgecss.config.js", 14 | process.cwd() + "/forgecss.config.mjs" 15 | ]; 16 | 17 | program.option("-c, --config ,", "Path to forgecss config file", POSSIBLE_CONFIG_FILES[0]); 18 | program.option("-w, --watch", "Enable watch mode", false); 19 | program.option("-v, --verbose", "Enable watch mode", false); 20 | program.parse(); 21 | 22 | const options = program.opts(); 23 | let config = null, instance = null; 24 | 25 | if (!fs.existsSync(options.config)) { 26 | let found = false; 27 | for (let possibleConfigFile of POSSIBLE_CONFIG_FILES) { 28 | if (fs.existsSync(possibleConfigFile)) { 29 | options.config = possibleConfigFile; 30 | found = true; 31 | break; 32 | } 33 | } 34 | if (!found) { 35 | throw new Error(`forgecss: config file not found at ${options.config} or any of the default locations.`); 36 | } 37 | } 38 | 39 | async function loadConfig(configPath) { 40 | const abs = path.resolve(configPath); 41 | if (abs.toLowerCase().endsWith('.json')) { 42 | const jsonStr = fs.readFileSync(abs, "utf-8"); 43 | if (options.verbose) { 44 | console.log(`forgecss: Loaded config file from ${abs.replace(process.cwd(), '')}`); 45 | } 46 | try { 47 | return JSON.parse(jsonStr); 48 | } catch(err) { 49 | throw new Error(`forgecss: error parsing config file at ${configPath}: ${err}`); 50 | } 51 | } else { 52 | const module = await import(pathToFileURL(abs).href); 53 | if (options.verbose) { 54 | console.log(`forgecss: Loaded config file from ${abs.replace(process.cwd(), '')}`); 55 | } 56 | return module.default || module; 57 | } 58 | } 59 | async function runForgeCSS(lookAtPath = null) { 60 | if (!config) { 61 | // The very first run 62 | config = await loadConfig(options.config); 63 | if (!config.dir) { 64 | throw new Error('forgecss: missing "dir" in configuration.'); 65 | } 66 | if (!config.output) { 67 | throw new Error('forgecss: missing "output" in configuration.'); 68 | } 69 | instance = ForgeCSS(config); 70 | if (options.watch) { 71 | const watcher = chokidar.watch(config.dir, { 72 | persistent: true, 73 | ignoreInitial: true, 74 | ignored: (p, stats) => path.resolve(p) === path.resolve(config.output) 75 | }); 76 | watcher.on("change", async (filePath) => { 77 | if (options.verbose) { 78 | console.log(`forgecss: Detected change in ${filePath}`); 79 | } 80 | runForgeCSS(filePath); 81 | }); 82 | if (options.verbose) { 83 | console.log("forgecss: Watch mode enabled. Listening for file changes..."); 84 | } 85 | } 86 | } 87 | if (lookAtPath) { 88 | instance.parseFile({ file: lookAtPath, output: config.output }); 89 | } else { 90 | instance.parseDirectory({ dir: config.dir, output: config.output }); 91 | } 92 | } 93 | 94 | runForgeCSS(); 95 | 96 | -------------------------------------------------------------------------------- /packages/site/src/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect } from "react"; 2 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 3 | import "monaco-editor/esm/vs/basic-languages/html/html.contribution"; 4 | import "monaco-editor/esm/vs/basic-languages/css/css.contribution"; 5 | import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"; 6 | 7 | monaco.editor.defineTheme("fcss", { 8 | base: "vs-dark", // or 'vs' 9 | inherit: true, 10 | rules: [ 11 | { token: "comment", foreground: "6A9955" }, 12 | { token: "string", foreground: "CE9178" }, 13 | { token: "keyword", foreground: "C586C0" } 14 | ], 15 | colors: { 16 | "editor.background": "#292929", 17 | "editor.foreground": "#d4d4d4", 18 | "editorCursor.foreground": "#ffffff", 19 | "editor.lineHighlightBackground": "#1e1e1e", 20 | "editorLineNumber.foreground": "#5a5a5a", 21 | "editor.selectionBackground": "#264f78", 22 | "editor.inactiveSelectionBackground": "#3a3d41" 23 | } 24 | }); 25 | type EditorProps = { 26 | language: string; 27 | code: string; 28 | className?: string, 29 | onChange?: (code: string) => void, 30 | readonly?: boolean 31 | }; 32 | 33 | export function Editor({ language, code, className, onChange, readonly }: EditorProps) { 34 | const [editor, setEditor] = useState(null); 35 | const monacoEl = useRef(null); 36 | 37 | useEffect(() => { 38 | if (monacoEl) { 39 | setEditor((editor) => { 40 | if (editor) return editor; 41 | 42 | monaco.editor.setTheme("fcss"); 43 | 44 | const editorInstance = monaco.editor.create(monacoEl.current!, { 45 | value: code, 46 | language: language || "javascript", 47 | minimap: { enabled: false }, 48 | lineNumbers: "off", 49 | glyphMargin: false, 50 | folding: false, 51 | lineDecorationsWidth: 0, 52 | lineNumbersMinChars: 0, 53 | scrollbar: { 54 | vertical: "auto", 55 | horizontal: "hidden" 56 | }, 57 | overviewRulerLanes: 0, 58 | fontSize: 14, 59 | lineHeight: 24, 60 | letterSpacing: 0.4, 61 | contextmenu: false, 62 | renderLineHighlight: "none", 63 | cursorBlinking: "smooth", 64 | automaticLayout: true, 65 | tabSize: 2, 66 | insertSpaces: true, 67 | readOnly: readonly, 68 | wordWrap: "on" 69 | }); 70 | editorInstance.onDidChangeModelContent(() => { 71 | onChange && onChange(editorInstance.getValue()); 72 | }); 73 | return editorInstance; 74 | }); 75 | } 76 | 77 | return () => { 78 | editor?.dispose(); 79 | } 80 | }, [monacoEl.current]); 81 | 82 | useEffect(() => { 83 | if (!editor) return; 84 | if (editor.getValue() !== code) { 85 | editor.setValue(code); 86 | } 87 | const model = editor.getModel(); 88 | if (model) { 89 | monaco.editor.setModelLanguage(model, language); 90 | } 91 | }, [code, language]) 92 | 93 | return ( 94 |
95 |
96 |
97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /packages/site/src/Syntax.tsx: -------------------------------------------------------------------------------- 1 | import fx from "forgecss/fx"; 2 | 3 | const EXAMPLES = [ 4 | { 5 | input: ( 6 | 7 | foo bar 8 | 9 | ), 10 | outputHTML: foo bar, 11 | outputCSS: -, 12 | text: ( 13 | 14 | foo and bar are just tokens here. No CSS is 15 | generated. 16 | 17 | ) 18 | }, 19 | { 20 | input: ( 21 | 22 | hover:bar 23 | 24 | ), 25 | outputHTML: hover_bar, 26 | outputCSS: ( 27 | 28 | hover_bar:hover {"{"} ... {"}"} 29 | 30 | ), 31 | text: ( 32 | 33 | If hover is a valid pseudo class and bar is a 34 | valid utility, CSS will be generated for the hover state applying the styles set in{" "} 35 | bar class. 36 | 37 | ) 38 | }, 39 | { 40 | input: ( 41 | 42 | desktop:bar 43 | 44 | ), 45 | outputHTML: desktop_bar, 46 | outputCSS: ( 47 | 48 | @media (min-width: 780px) {"{"} 49 |
50 | 51 | desktop_bar {"{"} ... {"}"} 52 | 53 |
54 | {"}"} 55 |
56 | ), 57 | text: ( 58 | 59 | If desktop is a valid breakpoint (defined into the ForgeCSS configuration) and bar is a 60 | valid utility, media query will be generated for the created class desktop_bar with the styles set in bar class. 61 | 62 | ) 63 | } 64 | ]; 65 | 66 | export default function Syntax() { 67 | return ( 68 |
69 |

syntax

70 | {EXAMPLES.map((example, i) => { 71 | return ( 72 |
73 |
74 |
75 | {example.input} 76 |
77 |
78 |
79 | {example.outputHTML} 80 |
81 |
82 | {example.outputCSS} 83 |
84 |
85 | {example.text} 86 |
87 |
88 |
89 |
90 | ); 91 | })} 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /packages/forgecss/index.browser.js: -------------------------------------------------------------------------------- 1 | import { 2 | extractStyles, 3 | getStylesByClassName, 4 | invalidateInventory, 5 | resolveApplys, 6 | getInventory, 7 | getAllCSS 8 | } from "./lib/inventory.js"; 9 | import { invalidateUsageCache, findUsages, getUsages } from "./lib/usages.js"; 10 | import fx from './lib/fx.js' 11 | import { astToRules, rulesToCSS } from "./lib/forge-lang/Compiler.js"; 12 | import { toAST } from "./lib/forge-lang/Parser.js"; 13 | import { DEFAULT_OPTIONS } from "./lib/constants.js"; 14 | 15 | function ForgeCSS(options) { 16 | const config = { ...DEFAULT_OPTIONS }; 17 | 18 | config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {}); 19 | config.usageAttributes = options?.usageAttributes ?? DEFAULT_OPTIONS.usageAttributes; 20 | config.verbose = options?.verbose ?? DEFAULT_OPTIONS.verbose; 21 | config.minify = options?.minify ?? DEFAULT_OPTIONS.minify; 22 | config.bundleAll = options?.bundleAll ?? DEFAULT_OPTIONS.bundleAll; 23 | 24 | async function result() { 25 | try { 26 | const cache = {}; 27 | const usages = getUsages(); 28 | const ast = toAST( 29 | Object.values(usages).reduce((acc, i) => { 30 | return acc.concat(i); 31 | }, []) 32 | ); 33 | let rules = astToRules(ast, { 34 | getStylesByClassName, 35 | cache, 36 | config 37 | }); 38 | rules.push(resolveApplys()); 39 | let css = rulesToCSS(rules.filter(Boolean), config); 40 | if (config.bundleAll) { 41 | css = getAllCSS() + "\n" + css; 42 | } 43 | if (config.minify) { 44 | css = minifyCSS(css); 45 | } 46 | if (config.verbose) { 47 | console.log("forgecss: output CSS generated successfully."); 48 | } 49 | return css; 50 | } catch (err) { 51 | console.error(`forgecss: error generating output CSS: ${err}`); 52 | } 53 | return null; 54 | } 55 | 56 | return { 57 | async parse({ css, html, jsx }) { 58 | if (!css) { 59 | throw new Error('forgecss: parse requires "css".'); 60 | } 61 | if (!html && !jsx) { 62 | throw new Error('forgecss: parse requires "html" or "jsx".'); 63 | } 64 | invalidateInventory(); 65 | invalidateUsageCache(); 66 | // filling the inventory 67 | try { 68 | extractStyles("styles.css", css); 69 | } catch (err) { 70 | console.error(`forgecss: error extracting styles.`, err); 71 | } 72 | // finding the usages 73 | try { 74 | if (html) { 75 | await findUsages("usage.html", html); 76 | } else if (jsx) { 77 | await findUsages("usage.jsx", jsx); 78 | } 79 | } catch (err) { 80 | console.error(`forgecss: error extracting usages.`, err); 81 | } 82 | return result(); 83 | }, 84 | fxAll: function (root) { 85 | const rootNode = root || document; 86 | const nodes = rootNode.querySelectorAll("[class]"); 87 | 88 | for (let i = 0; i < nodes.length; i++) { 89 | let el = nodes[i]; 90 | let original = el.getAttribute("class"); 91 | if (!original) continue; 92 | 93 | let transformed = fx(original); 94 | 95 | if (typeof transformed === "string" && transformed !== original) { 96 | el.setAttribute("class", transformed); 97 | } 98 | } 99 | }, 100 | fx, 101 | getUsages, 102 | getStylesByClassName, 103 | getInventory 104 | }; 105 | } 106 | 107 | window.ForgeCSS = ForgeCSS; -------------------------------------------------------------------------------- /packages/site/public/forgecss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import './forgecss.css'; 3 | import fx from '../../../packages/forgecss/fx' 4 | 5 | function App() { 6 | const [ progress, setProgress ] = useState(false); 7 | const [ success, setSuccess ] = useState(false); 8 | const [theme, setTheme ] = useState<'light' | 'dark'>('dark'); 9 | const [errors, setErrors ] = useState<{ username: boolean | string, password: boolean | string }>({ username: false, password: false }); 10 | 11 | async function submit(e) { 12 | e.preventDefault(); 13 | setProgress(true); 14 | if (e.target.username.value === '') { 15 | setErrors(errors => ({ ...errors, username: 'Username is required' })); 16 | setProgress(false); 17 | return; 18 | } 19 | if (e.target.password.value === "") { 20 | setErrors((errors) => ({ ...errors, password: "Password is required" })); 21 | setProgress(false); 22 | return; 23 | } 24 | await new Promise((resolve) => setTimeout(resolve, 2000)); 25 | setSuccess(true); 26 | setProgress(false); 27 | } 28 | function clearErrors(field: string) { 29 | setErrors((errors) => ({ ...errors, [field]: false })); 30 | } 31 | 32 | const isErrored = Boolean(errors.username || errors.password); 33 | 34 | return ( 35 |
36 |
37 |
47 | {success ? ( 48 |

You are ready to go!

49 | ) : ( 50 | <> 51 | 66 | 80 | 87 | 88 | )} 89 |
90 |
91 |
92 | 96 | 100 |
101 |
102 | ); 103 | } 104 | 105 | export default App 106 | -------------------------------------------------------------------------------- /packages/forgecss/lib/forge-lang/Compiler.js: -------------------------------------------------------------------------------- 1 | import postcss from "postcss"; 2 | import { NODE_TYPE, ALLOWED_PSEUDO_CLASSES } from "./constants.js"; 3 | import { minifyCSS } from './utils.js' 4 | import { normalizeLabel } from "../fx.js"; 5 | 6 | export function astToRules(ast, options) { 7 | let rules = []; 8 | const { getStylesByClassName, cache = {}, config } = options 9 | // console.log( 10 | // "\n====================================================================== ^\n", 11 | // JSON.stringify(ast, null, 2), 12 | // "\n====================================================================== $\n" 13 | // ); 14 | 15 | for(let node of ast) { 16 | switch (node.type) { 17 | case NODE_TYPE.TOKEN: 18 | // ignoring ... just tokens 19 | break; 20 | case NODE_TYPE.VARIANT: 21 | let variantSelector = node.selector; 22 | let classes = (node?.payload?.value ?? "").split(",").map((c) => c.trim()).filter(Boolean); 23 | let childRules; 24 | if (!node.payload.value && typeof node.payload === 'object') { 25 | childRules = astToRules([node.payload], options); 26 | } 27 | 28 | // -------------------------------------------------------- pseudo 29 | if (ALLOWED_PSEUDO_CLASSES.includes(variantSelector)) { 30 | classes.forEach(cls => { 31 | let selector = `.${variantSelector}_${cls}`; 32 | const rule = createRule(`${selector}:${variantSelector}`, cls, cache); 33 | if (rule) { 34 | rules.push(rule); 35 | } 36 | }); 37 | // -------------------------------------------------------- media queries 38 | } else if (config.breakpoints[variantSelector]) { 39 | let mediaRule; 40 | if (cache[config.breakpoints[variantSelector]]) { 41 | mediaRule = cache[config.breakpoints[variantSelector]]; 42 | } else { 43 | mediaRule = cache[config.breakpoints[variantSelector]] = postcss.atRule({ 44 | name: "media", 45 | params: config.breakpoints[variantSelector] 46 | }); 47 | rules.push(mediaRule); 48 | } 49 | if (childRules) { 50 | childRules.forEach(r => { 51 | mediaRule.append(r); 52 | }) 53 | } else { 54 | classes.forEach((cls) => { 55 | let selector = `.${variantSelector}_${cls}`; 56 | const rule = createRule(selector, cls, cache); 57 | if (rule) { 58 | mediaRule.append(rule); 59 | } 60 | }); 61 | } 62 | } else if (node.payload?.type === NODE_TYPE.TOKEN && node.simple === true) { 63 | console.warn(`forgecss: there is no breakpoint defined for label "${variantSelector}".`); 64 | // -------------------------------------------------------- arbitrary 65 | } else { 66 | classes.forEach(cls => { 67 | if (Array.isArray(variantSelector)) { 68 | variantSelector = variantSelector 69 | .map(({ type, value, selector, payload }) => { 70 | if (type === "token") { 71 | return value; 72 | } 73 | }) 74 | .filter(Boolean) 75 | .join(" "); 76 | } 77 | if (["", "true"].includes(variantSelector)) { 78 | return; 79 | } 80 | const I = normalizeLabel(variantSelector) + "_" + cls; 81 | const selector = evaluateArbitrary(variantSelector, I); 82 | const rule = createRule(selector, cls, cache); 83 | if (rule) { 84 | rules.push(rule); 85 | } 86 | }) 87 | } 88 | break; 89 | } 90 | } 91 | 92 | function createRule(selector, pickStylesFrom, cache = {}) { 93 | if (cache[selector]) { 94 | return; 95 | } 96 | const newRule = cache[selector] = postcss.rule({ selector }); 97 | const decls = getStylesByClassName(pickStylesFrom); 98 | if (decls.length === 0) { 99 | return; 100 | } 101 | decls.forEach((d) => { 102 | newRule.append( 103 | postcss.decl({ 104 | prop: d.prop, 105 | value: d.value, 106 | important: d.important 107 | }) 108 | ); 109 | }); 110 | return newRule; 111 | } 112 | function evaluateArbitrary(variant, I) { 113 | variant = variant.replace(/[&]/g, `.${I}`); 114 | return variant; 115 | } 116 | 117 | return rules; 118 | } 119 | 120 | export function rulesToCSS(rules) { 121 | return rules.map((r) => r.toString()).join("\n"); 122 | } -------------------------------------------------------------------------------- /packages/site/public/styles.css: -------------------------------------------------------------------------------- 1 | :root{--background-dark:#1b1b1b;--background:#292929;--background-light:#595959;--background-light2:#7b7b7b;--text:#f0f0f0;--warning:#df6464;--success:#61e481;--bit1:#8ff08f;--bit2:#e65f5f;--bit3:#909eee}*{box-sizing:border-box}body{background:var(--background);color:var(--text);font-family:"Arimo",sans-serif;font-optical-sizing:auto;font-weight:200;font-style:normal;font-size:20px;line-height:1.4em;margin:0;padding:0;width:100%;height:100%}h1,h2,h3,p,ul{margin:0;padding:0;line-height:1.2em}h1,h2,h3{font-family:"Alfa Slab One",serif;font-weight:400;font-style:normal}.warning{color:var(--warning)}.success{color:var(--success)}.paler{color:var(--background-light2)}.p1{padding:1rem}.p2{padding:2rem}.p3{padding:3rem}.pt2{padding-top:2rem}.pl1{padding-left:1rem}.py1{padding-top:1rem;padding-bottom:1rem}.py2{padding-top:2rem;padding-bottom:2rem}.py3{padding-top:3rem;padding-bottom:3rem}.pl3{padding-left:3rem}.pr3{padding-right:3rem}.pt3{padding-top:3rem}.mt1{margin-top:1rem}.mt2{margin-top:2rem}.mt3{margin-top:3rem}.my3{margin-top:3rem;margin-bottom:3rem}.mxauto{margin-left:auto;margin-right:auto}.grid2{display:grid;grid-template-columns:1fr 1fr}.grid2x1{display:grid;grid-template-columns:2fr 1fr}.b{display:block}.flex{display:flex}.flex-center{display:flex;align-items:center;justify-content:center}.flex-v-center{display:flex;align-items:center}.flex-col{display:flex;flex-direction:column}.flex1{flex:1}.span2{grid-column:span 2}.gap05{gap:0.5rem}.gap1{gap:1rem}.gap2{gap:2rem}.maxw600{max-width:600px}.maxw800{max-width:800px}.maxw1000{max-width:1000px}.maxw1400{max-width:1400px}.minh500{min-height:500px}.bg{background:var(--background)}.bg-dark{background:var(--background-dark)}.black-bg{background:black}.fz1{font-size:1rem}.fz15{font-size:1.5rem}.fz2{font-size:2rem}.fz3{font-size:3rem}.fw100{font-weight:100}.hidden{display:none}.tac{text-align:center}.border-t{border-top:solid 1px var(--background-light)}.border-l{border-left:solid 1px var(--background-light)}.border-r{border-right:solid 1px var(--background-light)}.border-b{border-bottom:solid 1px var(--background-light)}.border{border:solid 1px var(--background-light)}header{background-color:#292929;opacity:1;background:radial-gradient(circle,transparent 20%,#292929 20%,#292929 80%,transparent 80%,transparent),radial-gradient(circle,transparent 20%,#292929 20%,#292929 80%,transparent 80%,transparent) 10px 10px,linear-gradient(#000000 0.8px,transparent 0.8px) 0 -0.4px,linear-gradient(90deg,#000000 0.8px,#292929 0.8px) -0.4px 0;background-size:20px 20px,20px 20px,10px 10px,10px 10px}.hero{background:radial-gradient(125% 125% at 50% 10%,#000000 40%,#031724 100%);border-top:solid 1px #575757;border-bottom:solid 1px #272727}.hero nav a{text-align:center;color:var(--text);text-decoration:none;font-size:1rem;background:radial-gradient(125% 125% at 50% 10%,#434343 40%,#000000 100%);padding:1rem 2rem;border-radius:4px}.hero nav a:hover{background:radial-gradient(125% 125% at 50% 10%,#595959 40%,#000000 100%)}hr{border-top:solid 4px #000;border-bottom:none;border-left:none;border-right:none;}.files{list-style:none;display:flex}.files li{font-size:0.8rem;border-top:solid 2px var(--background-light2);border-right:solid 2px var(--background-light2)}.files li:first-child{border-left:solid 2px var(--background-light2)}.files li button{display:block;background:none;border:none;color:var(--text);font-size:1rem;cursor:pointer;padding:0.5rem 1rem;opacity:0.3}.files li.selected button{background:var(--background-light);filter:drop-shadow(0px 2px 0px white);opacity:1}.editor-wrapper{border:solid 2px var(--background-light);background:var(--background);height:520px;overflow-y:auto}.editor{width:100%;height:100%;min-height:400px}.syntax-example .input,.syntax-example .output{position:relative}.syntax-example .input::before,.syntax-example .output::before{display:block;font-size:0.8rem;position:absolute;top:14px;line-height:0;color:var(--background-light2)}.syntax-example .input::before{content:attr(data-label);left:14px}.syntax-example .output::before{content:attr(data-label);right:14px}.syntax-example code{font-size:0.9rem;font-family:inherit;padding:0.2rem 0.2rem;border-radius:4px}.code1{border-bottom:solid 1px var(--bit1)}.code2{border-bottom:solid 1px var(--bit2)}.code3{border-bottom:solid 1px var(--bit3)}.bit1,.bit2,.bit3{position:relative;padding:0 4px 4px 4px}.bit1::before,.bit2::before,.bit3::before{content:"";position:absolute;top:0;left:0;width:2px;height:4px}.bit1::after,.bit2::after,.bit3::after{content:"";position:absolute;top:0;right:0;width:2px;height:4px}.bit1{border-top:solid 1px var(--bit1)}.bit1::after,.bit1::before{background:var(--bit1)}.bit2{border-top:solid 1px var(--bit2)}.bit2::after,.bit2::before{background:var(--bit2)}.bit3{border-top:solid 1px var(--bit3)}.bit3::after,.bit3::before{background:var(--bit3)}:not(pre)>code[class*=language-],pre[class*=language-]{background:none !important;font-size:0.9rem !important}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#569cd6 !important}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#9cdcfe !important}.token.punctuation{color:#808080 !important}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#ce9178 !important}code{white-space:break-spaces !important;word-break:break-word !important}@media all and (min-width:780px){.desktop_py3{padding-top:3rem;padding-bottom:3rem}.desktop_fz3{font-size:3rem}.desktop_mt2{margin-top:2rem}.desktop_grid2{display:grid;grid-template-columns:1fr 1fr}.desktop_pt3{padding-top:3rem}.desktop_pl3{padding-left:3rem}.desktop_pr3{padding-right:3rem}}@media all and (max-width:779px){.mobile_p1{padding:1rem}.mobile_b{display:block}.mobile_mt1{margin-top:1rem}.mobile_mt2{margin-top:2rem}.mobile_hidden{display:none}} -------------------------------------------------------------------------------- /packages/forgecss/lib/forge-lang/Parser.js: -------------------------------------------------------------------------------- 1 | export function toAST(input, cache = {}) { 2 | if (cache[input]) return cache[input]; 3 | 4 | if (Array.isArray(input)) { 5 | const optimized = []; 6 | input.forEach((str) => { 7 | str 8 | .trim() 9 | .split(" ") 10 | .forEach((part) => { 11 | if (!optimized.includes(part)) optimized.push(part); 12 | }); 13 | }); 14 | input = optimized.join(" "); 15 | } 16 | 17 | const s = String(input ?? "").trim(); 18 | let i = 0; 19 | 20 | const isWS = (ch) => ch === " " || ch === "\n" || ch === "\t" || ch === "\r"; 21 | 22 | function skipWS() { 23 | while (i < s.length && isWS(s[i])) i++; 24 | } 25 | 26 | function parseSequence(stopChar) { 27 | const nodes = []; 28 | while (i < s.length) { 29 | skipWS(); 30 | if (stopChar && s[i] === stopChar) break; 31 | if (i >= s.length) break; 32 | nodes.push(parseItem()); 33 | } 34 | return nodes; 35 | } 36 | 37 | function readIdentUntilDelimiter() { 38 | let out = ""; 39 | while (i < s.length) { 40 | const ch = s[i]; 41 | // stop at whitespace, "(", ")", ":" (variant separator) 42 | if (isWS(ch) || ch === "(" || ch === ")" || ch === ":") break; 43 | // IMPORTANT: DO NOT consume "[" here; it may be: 44 | // - leading bracket variant (handled in parseItem when ch === "[") 45 | // - attribute selector suffix (handled in parseItem after reading head) 46 | if (ch === "[") break; 47 | 48 | out += ch; 49 | i++; 50 | } 51 | return out.trim(); 52 | } 53 | 54 | function isVariantLabel(str) { 55 | return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str); 56 | } 57 | 58 | function isCallName(str) { 59 | return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str); 60 | } 61 | 62 | function parseItem() { 63 | skipWS(); 64 | const ch = s[i]; 65 | 66 | // Bracket variant: [selector]:payload 67 | if (ch === "[") { 68 | const selectorRaw = parseBracketContent(); // returns content WITHOUT outer [] 69 | const selectorAst = toAST(selectorRaw, cache); 70 | 71 | if (s[i] === ":") { 72 | i++; 73 | const payload = parseItem(); 74 | return { type: "variant", selector: selectorAst, payload }; 75 | } 76 | 77 | // If it's just a standalone bracket chunk (not a variant), 78 | // keep it as a token string. (You can change this if you prefer AST here.) 79 | return { type: "token", value: `[${selectorRaw}]` }; 80 | } 81 | 82 | // Read label/name/token 83 | let head = readIdentUntilDelimiter(); 84 | 85 | // NEW: absorb attribute selector suffixes: foo[...][...] 86 | // This handles &\[type=...\] and similar. 87 | while (s[i] === "[") { 88 | const inner = parseBracketContent(); // consumes the bracket block 89 | head += `[${inner}]`; 90 | } 91 | 92 | // Label variant: hover:..., desktop:..., focus:... 93 | if (s[i] === ":" && isVariantLabel(head)) { 94 | i++; // consume ":" 95 | const payload = parseItem(); 96 | return { type: "variant", selector: head, payload, simple: true }; 97 | } 98 | 99 | // Call: name(...) 100 | if (s[i] === "(" && isCallName(head)) { 101 | i++; // consume "(" 102 | const args = []; 103 | while (i < s.length) { 104 | skipWS(); 105 | if (s[i] === ")") { 106 | i++; 107 | break; 108 | } 109 | args.push(parseItem()); 110 | skipWS(); 111 | if (s[i] === ",") i++; 112 | } 113 | return { type: "call", name: head, args }; 114 | } 115 | 116 | if (s[i] === ":") { 117 | head += ":"; 118 | i++; // consume ":" 119 | 120 | // absorb following identifier / call / selector chunk 121 | while (i < s.length) { 122 | const ch = s[i]; 123 | if (isWS(ch) || ch === ")" || ch === ",") break; 124 | 125 | if (ch === "[") { 126 | const inner = parseBracketContent(); 127 | head += `[${inner}]`; 128 | continue; 129 | } 130 | 131 | if (ch === "(") { 132 | head += "("; 133 | i++; 134 | let depth = 1; 135 | while (i < s.length && depth > 0) { 136 | if (s[i] === "(") depth++; 137 | if (s[i] === ")") depth--; 138 | head += s[i++]; 139 | } 140 | continue; 141 | } 142 | 143 | head += ch; 144 | i++; 145 | } 146 | } 147 | 148 | return { type: "token", value: head }; 149 | } 150 | 151 | function parseBracketContent() { 152 | // assumes s[i] === "[" 153 | i++; // consume "[" 154 | let out = ""; 155 | let bracket = 1; 156 | let quote = null; 157 | 158 | while (i < s.length) { 159 | const ch = s[i]; 160 | 161 | if (quote) { 162 | out += ch; 163 | if (ch === "\\" && i + 1 < s.length) { 164 | i++; 165 | out += s[i]; 166 | } else if (ch === quote) { 167 | quote = null; 168 | } 169 | i++; 170 | continue; 171 | } 172 | 173 | if (ch === "'" || ch === '"') { 174 | quote = ch; 175 | out += ch; 176 | i++; 177 | continue; 178 | } 179 | 180 | if (ch === "[") { 181 | bracket++; 182 | out += ch; 183 | i++; 184 | continue; 185 | } 186 | 187 | if (ch === "]") { 188 | bracket--; 189 | if (bracket === 0) { 190 | i++; // consume final "]" 191 | break; 192 | } 193 | out += ch; 194 | i++; 195 | continue; 196 | } 197 | 198 | out += ch; 199 | i++; 200 | } 201 | 202 | return out; 203 | } 204 | 205 | const ast = parseSequence(null); 206 | cache[input] = ast; 207 | return ast; 208 | } 209 | -------------------------------------------------------------------------------- /packages/forgecss/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { writeFile } from "fs/promises"; 3 | import chokidar from "chokidar"; 4 | 5 | import { extractStyles, getStylesByClassName, invalidateInventory, resolveApplys, getAllCSS } from "./lib/inventory.js"; 6 | import { invalidateUsageCache, findUsages, getUsages } from "./lib/usages.js"; 7 | import { astToRules, rulesToCSS } from './lib/forge-lang/Compiler.js'; 8 | import { toAST } from './lib/forge-lang/Parser.js'; 9 | import { minifyCSS } from './lib/forge-lang/utils.js'; 10 | import { getAllFiles, JSXParser, readFileContent } from './lib/helpers.js'; 11 | import { DEFAULT_OPTIONS } from './lib/constants.js'; 12 | 13 | export default function ForgeCSS(options) { 14 | const config = { ...DEFAULT_OPTIONS }; 15 | 16 | config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {}); 17 | config.inventoryFiles = options?.inventoryFiles ?? DEFAULT_OPTIONS.inventoryFiles; 18 | config.usageFiles = options?.usageFiles ?? DEFAULT_OPTIONS.usageFiles; 19 | config.usageAttributes = options?.usageAttributes ?? DEFAULT_OPTIONS.usageAttributes; 20 | config.verbose = options?.verbose ?? DEFAULT_OPTIONS.verbose; 21 | config.minify = options?.minify ?? DEFAULT_OPTIONS.minify; 22 | config.bundleAll = options?.bundleAll ?? DEFAULT_OPTIONS.bundleAll; 23 | 24 | async function result(output) { 25 | try { 26 | const cache = {}; 27 | const usages = getUsages(); 28 | const ast = toAST( 29 | Object.values(usages).reduce((acc, i) => { 30 | return acc.concat(i); 31 | }, []) 32 | ); 33 | let rules = astToRules(ast, { 34 | getStylesByClassName, 35 | cache, 36 | config 37 | }); 38 | rules.push(resolveApplys()); 39 | let css = rulesToCSS(rules.filter(Boolean), config); 40 | if (config.bundleAll) { 41 | css = getAllCSS() + "\n" + css; 42 | } 43 | if (config.minify) { 44 | css = minifyCSS(css); 45 | } 46 | if (output) { 47 | await writeFile(output, css, "utf-8"); 48 | } 49 | if (config.verbose) { 50 | console.log("forgecss: output CSS generated successfully."); 51 | } 52 | return css; 53 | } catch (err) { 54 | console.error(`forgecss: error generating output CSS: ${err}`); 55 | } 56 | return null; 57 | } 58 | function runWatcher(what, output, callback) { 59 | const watcher = chokidar.watch(what, { 60 | persistent: true, 61 | ignoreInitial: true, 62 | ignored: (p, stats) => output && path.resolve(p) === path.resolve(output) 63 | }); 64 | watcher.on("change", async (filePath) => { 65 | if (config.verbose) { 66 | invalidateUsageCache(filePath) 67 | invalidateInventory(filePath); 68 | console.log(`forgecss: Detected change in ${filePath}`); 69 | } 70 | callback(); 71 | }); 72 | if (config.verbose) { 73 | console.log("forgecss: Watch mode enabled. Listening for file changes..."); 74 | } 75 | } 76 | 77 | return { 78 | async parseDirectory({ dir, output = null, watch = false }) { 79 | if (!dir) { 80 | throw new Error('forgecss: parseDirectory requires "dir" as an argument.'); 81 | } 82 | try { 83 | // filling the inventory 84 | let files = await getAllFiles(dir, config.inventoryFiles); 85 | for (let file of files) { 86 | extractStyles(file, await readFileContent(file)); 87 | } 88 | } catch (err) { 89 | console.error(`forgecss: error extracting styles.`, err); 90 | } 91 | // finding the usages 92 | try { 93 | let files = await getAllFiles(dir, config.usageFiles); 94 | for (let file of files) { 95 | await findUsages(file, await readFileContent(file), JSXParser); 96 | } 97 | } catch (err) { 98 | console.error(`forgecss: error extracting usages`, err); 99 | } 100 | watch && runWatcher(dir, output, () => { 101 | this.parseDirectory({ dir, output, watch: false }); 102 | }); 103 | // generating the output CSS 104 | return result(output); 105 | }, 106 | async parseFile({ file, output = null, watch = false }) { 107 | if (!file) { 108 | throw new Error('forgecss: parseFile requires "file" as an argument.'); 109 | } 110 | const ext = file.split(".").pop().toLowerCase(); 111 | // filling the inventory 112 | try { 113 | if (config.inventoryFiles.includes(ext)) { 114 | extractStyles(file, await readFileContent(file)); 115 | } 116 | } catch (err) { 117 | console.error(`forgecss: error extracting styles.`, err); 118 | } 119 | // finding the usages 120 | try { 121 | if (config.usageFiles.includes(ext)) { 122 | invalidateUsageCache(file); 123 | await findUsages(file, await readFileContent(file), JSXParser); 124 | } 125 | } catch (err) { 126 | console.error(`forgecss: error extracting usages.`, err); 127 | } 128 | watch && runWatcher(file, output, () => { 129 | this.parseFile({ file, output, watch: false }); 130 | }); 131 | // generating the output CSS 132 | return result(output); 133 | }, 134 | async parse({ css, html, jsx, output = null }) { 135 | if (!css) { 136 | throw new Error('forgecss: parse requires "css".'); 137 | } 138 | if (!html && !jsx) { 139 | throw new Error('forgecss: parse requires "html" or "jsx".'); 140 | } 141 | invalidateInventory(); 142 | invalidateUsageCache(); 143 | // filling the inventory 144 | try { 145 | extractStyles("styles.css", css); 146 | } catch (err) { 147 | console.error(`forgecss: error extracting styles.`, err); 148 | } 149 | // finding the usages 150 | try { 151 | if (html) { 152 | await findUsages("usage.html", html); 153 | } else if (jsx) { 154 | await findUsages("usage.jsx", jsx, JSXParser); 155 | } 156 | } catch (err) { 157 | console.error(`forgecss: error extracting usages.`, err); 158 | } 159 | return result(output); 160 | } 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /packages/site/public/input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | input -------------------------------------------------------------------------------- /packages/site/public/output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | output -------------------------------------------------------------------------------- /packages/site/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-dark: #1b1b1b; 3 | --background: #292929; 4 | --background-light: #595959; 5 | --background-light2: #7b7b7b; 6 | --text: #f0f0f0; 7 | --warning: #df6464; 8 | --success: #61e481; 9 | --bit1: #8ff08f; 10 | --bit2: #e65f5f; 11 | --bit3: #909eee; 12 | } 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | body { 18 | background: var(--background); 19 | color: var(--text); 20 | font-family: "Arimo", sans-serif; 21 | font-optical-sizing: auto; 22 | font-weight: 200; 23 | font-style: normal; 24 | font-size: 20px; 25 | line-height: 1.4em; 26 | margin: 0; 27 | padding: 0; 28 | width: 100%; 29 | height: 100%; 30 | } 31 | h1, h2, h3, p, ul { 32 | margin: 0; 33 | padding: 0; 34 | line-height: 1.2em; 35 | } 36 | h1, h2, h3 { 37 | font-family: "Alfa Slab One", serif; 38 | font-weight: 400; 39 | font-style: normal; 40 | } 41 | 42 | .warning { color: var(--warning); } 43 | .success { color: var(--success); } 44 | .paler { color: var(--background-light2); } 45 | .p1 { padding: 1rem; } 46 | .p2 { padding: 2rem; } 47 | .p3 { padding: 3rem; } 48 | .pt2 { padding-top: 2rem; } 49 | .pl1 { padding-left: 1rem; } 50 | .py1 { padding-top: 1rem; padding-bottom: 1rem; } 51 | .py2 { padding-top: 2rem; padding-bottom: 2rem; } 52 | .py3 { padding-top: 3rem; padding-bottom: 3rem; } 53 | .pl3 { padding-left: 3rem; } 54 | .pr3 { padding-right: 3rem; } 55 | .pt3 { padding-top: 3rem; } 56 | .mt1 { margin-top: 1rem; } 57 | .mt2 { margin-top: 2rem; } 58 | .mt3 { margin-top: 3rem; } 59 | .my3 { margin-top: 3rem; margin-bottom: 3rem; } 60 | .mxauto { margin-left: auto; margin-right: auto; } 61 | .grid2 { display: grid; grid-template-columns: 1fr 1fr; } 62 | .grid2x1 { display: grid; grid-template-columns: 2fr 1fr; } 63 | .b { display: block; } 64 | .flex { display: flex; } 65 | .flex-center { display: flex; align-items: center; justify-content: center; } 66 | .flex-v-center { display: flex; align-items: center; } 67 | .flex-col { display: flex; flex-direction: column; } 68 | .flex1 { flex: 1; } 69 | .span2 { grid-column: span 2; } 70 | .gap05 { gap: 0.5rem; } 71 | .gap1 { gap: 1rem; } 72 | .gap2 { gap: 2rem; } 73 | .maxw600 { max-width: 600px; } 74 | .maxw800 { max-width: 800px; } 75 | .maxw1000 { max-width: 1000px; } 76 | .maxw1400 { max-width: 1400px; } 77 | .minh500 { min-height: 500px; } 78 | .bg { background: var(--background); } 79 | .bg-dark { background: var(--background-dark); } 80 | .black-bg { background: black; } 81 | .fz1 { font-size: 1rem; } 82 | .fz15 { font-size: 1.5rem; } 83 | .fz2 { font-size: 2rem; } 84 | .fz3 { font-size: 3rem; } 85 | .fw100 { font-weight: 100; } 86 | .hidden { display: none; } 87 | .tac { text-align: center; } 88 | .border-t { border-top: solid 1px var(--background-light); } 89 | .border-l { border-left: solid 1px var(--background-light); } 90 | .border-r { border-right: solid 1px var(--background-light); } 91 | .border-b { border-bottom: solid 1px var(--background-light); } 92 | .border { border: solid 1px var(--background-light);} 93 | 94 | header { 95 | background-color: #292929; 96 | opacity: 1; 97 | background: radial-gradient(circle, transparent 20%, #292929 20%, #292929 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #292929 20%, #292929 80%, transparent 80%, transparent) 10px 10px, linear-gradient(#000000 0.8px, transparent 0.8px) 0 -0.4px, linear-gradient(90deg, #000000 0.8px, #292929 0.8px) -0.4px 0; 98 | background-size: 20px 20px, 20px 20px, 10px 10px, 10px 10px; 99 | } 100 | .hero { 101 | background: radial-gradient(125% 125% at 50% 10%, #000000 40%, #031724 100%); 102 | border-top: solid 1px #575757; 103 | border-bottom: solid 1px #272727; 104 | } 105 | .hero nav a { 106 | text-align: center; 107 | color: var(--text); 108 | text-decoration: none; 109 | font-size: 1rem; 110 | background: radial-gradient(125% 125% at 50% 10%, #434343 40%, #000000 100%); 111 | padding: 1rem 2rem; 112 | border-radius: 4px; 113 | } 114 | .hero nav a:hover { 115 | background: radial-gradient(125% 125% at 50% 10%, #595959 40%, #000000 100%); 116 | } 117 | hr { 118 | border-top: solid 4px #000; 119 | border-bottom: none; 120 | border-left: none; 121 | border-right: none;; 122 | } 123 | .files { 124 | list-style: none; 125 | display: flex; 126 | } 127 | .files li { 128 | font-size: 0.8rem; 129 | border-top: solid 2px var(--background-light2); 130 | border-right: solid 2px var(--background-light2); 131 | } 132 | .files li:first-child { 133 | border-left: solid 2px var(--background-light2); 134 | } 135 | .files li button { 136 | display: block; 137 | background: none; 138 | border: none; 139 | color: var(--text); 140 | font-size: 1rem; 141 | cursor: pointer; 142 | padding: 0.5rem 1rem; 143 | opacity: 0.3; 144 | } 145 | .files li.selected button { 146 | background: var(--background-light); 147 | filter: drop-shadow(0px 2px 0px white); 148 | opacity: 1; 149 | } 150 | .editor-wrapper { 151 | border: solid 2px var(--background-light); 152 | background: var(--background); 153 | height: 520px; 154 | overflow-y: auto; 155 | } 156 | .editor { 157 | width: 100%; 158 | height: 100%; 159 | min-height: 400px; 160 | } 161 | 162 | /* Syntax Example */ 163 | .syntax-example .input, 164 | .syntax-example .output { 165 | position: relative; 166 | } 167 | .syntax-example .input::before, 168 | .syntax-example .output::before { 169 | display: block; 170 | font-size: 0.8rem; 171 | position: absolute; 172 | top: 14px; 173 | line-height: 0; 174 | color: var(--background-light2); 175 | } 176 | .syntax-example .input::before { 177 | content: attr(data-label); 178 | left: 14px; 179 | } 180 | .syntax-example .output::before { 181 | content: attr(data-label); 182 | right: 14px; 183 | } 184 | .syntax-example code { 185 | font-size: 0.9rem; 186 | font-family: inherit; 187 | padding: 0.2rem 0.2rem; 188 | border-radius: 4px; 189 | } 190 | .code1 { 191 | border-bottom: solid 1px var(--bit1); 192 | } 193 | .code2 { 194 | border-bottom: solid 1px var(--bit2); 195 | } 196 | .code3 { 197 | border-bottom: solid 1px var(--bit3); 198 | } 199 | .bit1, .bit2, .bit3 { 200 | position: relative; 201 | padding: 0 4px 4px 4px; 202 | } 203 | .bit1::before, .bit2::before, .bit3::before { 204 | content: ""; 205 | position: absolute; 206 | top: 0; 207 | left: 0; 208 | width: 2px; 209 | height: 4px; 210 | } 211 | .bit1::after, .bit2::after, .bit3::after { 212 | content: ""; 213 | position: absolute; 214 | top: 0; 215 | right: 0; 216 | width: 2px; 217 | height: 4px; 218 | } 219 | .bit1 { 220 | border-top: solid 1px var(--bit1); 221 | } 222 | .bit1::after, .bit1::before { 223 | background: var(--bit1); 224 | } 225 | .bit2 { 226 | border-top: solid 1px var(--bit2); 227 | } 228 | .bit2::after, .bit2::before { 229 | background: var(--bit2); 230 | } 231 | .bit3 { 232 | border-top: solid 1px var(--bit3); 233 | } 234 | .bit3::after, .bit3::before { 235 | background: var(--bit3); 236 | } 237 | 238 | /* PrismJS Overrides */ 239 | :not(pre)>code[class*=language-], pre[class*=language-] { 240 | background: none !important; 241 | font-size: 0.9rem !important; 242 | } 243 | .token.constant, .token.deleted, .token.property, .token.symbol, .token.tag { 244 | color: #569cd6 !important; 245 | } 246 | .token.attr-name, .token.builtin, .token.char, .token.inserted, .token.selector, .token.string { 247 | color: #9cdcfe !important; 248 | } 249 | .token.punctuation { 250 | color: #808080 !important; 251 | } 252 | .token.atrule, .token.attr-value, .token.class-name, .token.function { 253 | color: #ce9178 !important; 254 | } 255 | code { 256 | white-space: break-spaces !important; 257 | word-break: break-word !important; 258 | } -------------------------------------------------------------------------------- /packages/site/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from 'react'; 2 | import fx from 'forgecss/fx' 3 | import { Editor } from './Editor'; 4 | import { ACTUAL_HTML_FILE, DEFAULT_FILES, DEFAULT_OUTPUT_FILES, TOTAL_CSS_FILE } from './constants'; 5 | import transformHtmlClassAttributes from './utils/transformHtmlClassAttributes'; 6 | import Syntax from './Syntax'; 7 | 8 | type File = { 9 | filename: string, 10 | content: string, 11 | selected: boolean, 12 | type: string 13 | } 14 | 15 | function filesReducer(state: File[], action: { type: string; payload?: any }) { 16 | if (action.type === "selected") { 17 | state = state.map((file, i) => ({ 18 | ...file, 19 | selected: action.payload === i 20 | })) 21 | } else if (action.type === "change") { 22 | const [filename, content] = action.payload; 23 | state = state.map((file) => ({ 24 | ...file, 25 | content: file.filename === filename ? content : file.content 26 | })) 27 | } 28 | return state; 29 | } 30 | 31 | function App() { 32 | const [inputFiles, updateInputFiles] = useReducer(filesReducer, DEFAULT_FILES); 33 | const [outputFiles, updateOutputFiles] = useReducer(filesReducer, DEFAULT_OUTPUT_FILES); 34 | const selectedInput = inputFiles.filter((f) => f.selected)[0]; 35 | const selectedOutput = outputFiles.filter((f) => f.selected)[0]; 36 | 37 | async function compile() { 38 | const css = inputFiles.filter((f) => f.filename === "styles.css")[0].content; 39 | const html = inputFiles.filter((f) => f.filename === "page.html")[0].content; 40 | let config = inputFiles.filter((f) => f.filename === "forgecss.config.json")[0].content; 41 | try { 42 | config = JSON.parse(config); 43 | // @ts-ignore 44 | config.minify = false; 45 | } catch(err) { 46 | console.error(err); 47 | return; 48 | } 49 | // @ts-ignore 50 | const forgecss = ForgeCSS(config); 51 | const result = await forgecss.parse({ css, html }); 52 | updateOutputFiles({ 53 | type: "change", 54 | payload: [TOTAL_CSS_FILE.filename, result] 55 | }); 56 | updateOutputFiles({ 57 | type: "change", 58 | payload: [ 59 | ACTUAL_HTML_FILE.filename, 60 | transformHtmlClassAttributes(html, (className: string) => { 61 | return fx(className) 62 | }) 63 | ] 64 | }); 65 | } 66 | 67 | useEffect(() => { 68 | compile(); 69 | }, [inputFiles]) 70 | 71 | useEffect(() => { 72 | setTimeout(() => { 73 | // @ts-ignore 74 | Prism.highlightAll(); 75 | }, 0); 76 | }, [outputFiles]) 77 | 78 | return ( 79 | <> 80 |
81 |
82 |
83 | ForgeCSS logo 84 |
85 |
86 |
87 |
88 |
89 |
90 |

ForgeCSS is a compiler for utility CSS classes.

91 |

92 | Compiler that understands CSS class syntax 93 |
94 | It parses class strings, applies rules and structure, and compiles them 95 | into CSS. 96 |

97 |

98 | What it isn’t! 99 | 100 | Not a CSS framework or a transformer 101 | 102 | 103 | Not utility library 104 | 105 | 106 | Not a Tailwind plugin 107 | 108 | 109 | Not a runtime style engine 110 | 111 | 112 | Not a CSS-in-JS solution 113 | 114 |

115 |

116 | ForgeCSS gives you the freedom to create your own utilities and compile them into CSS. 117 |

118 |
119 | 139 |
140 |
141 | ForgeCSS how-it-works diagram 147 |
148 |
149 | 150 |
151 |

Test it out!

152 |
153 |
154 |
155 | updateInputFiles({ type: "selected", payload: i })} /> 156 | updateInputFiles({ type: "change", payload: [selectedInput.filename, code] })} 161 | /> 162 |
163 | 164 |
165 |
166 |
167 | updateOutputFiles({ type: "selected", payload: i })} /> 168 |
169 |
170 |                   {selectedOutput.content}
171 |                 
172 |
173 |
174 | 175 |
176 |
177 |
178 |
179 |
180 | 181 | ); 182 | } 183 | 184 | function Tabs({ files, onClick }: { files: File[]; onClick: Function }) { 185 | return ( 186 |
    187 | {files.map((file, i) => { 188 | return ( 189 |
  • 190 | 191 |
  • 192 | ); 193 | })} 194 |
195 | ); 196 | } 197 | 198 | export default App 199 | -------------------------------------------------------------------------------- /packages/forgecss/tests/cases/00-lang/test.js: -------------------------------------------------------------------------------- 1 | import { toAST } from "../../../lib/forge-lang/Parser.js"; 2 | import { astToRules, rulesToCSS } from "../../../lib/forge-lang/Compiler.js"; 3 | import fx from '../../../lib/fx.js' 4 | import {minifyCSS} from "../../../lib/forge-lang/utils.js"; 5 | 6 | const mockGetStyleByClassName = (_) => [{ prop: "foo", value: "bar", important: false }]; 7 | 8 | export default function test() { 9 | const parserCases = [ 10 | { 11 | input: "btn primary hover:red", 12 | expected: [ 13 | { type: "token", value: "btn" }, 14 | { type: "token", value: "primary" }, 15 | { 16 | type: "variant", 17 | selector: "hover", 18 | payload: { 19 | type: "token", 20 | value: "red" 21 | }, 22 | simple: true 23 | } 24 | ] 25 | }, 26 | { 27 | input: "text [.dark &]:text-white mt1", 28 | expected: [ 29 | { type: "token", value: "text" }, 30 | { 31 | type: "variant", 32 | selector: [ 33 | { 34 | type: "token", 35 | value: ".dark" 36 | }, 37 | { 38 | type: "token", 39 | value: "&" 40 | } 41 | ], 42 | payload: { type: "token", value: "text-white" } 43 | }, 44 | { type: "token", value: "mt1" } 45 | ] 46 | }, 47 | { 48 | input: "foo layout(px4 rounded)", 49 | expected: [ 50 | { 51 | type: "token", 52 | value: "foo" 53 | }, 54 | { 55 | type: "call", 56 | name: "layout", 57 | args: [ 58 | { 59 | type: "token", 60 | value: "px4" 61 | }, 62 | { 63 | type: "token", 64 | value: "rounded" 65 | } 66 | ] 67 | } 68 | ] 69 | }, 70 | { 71 | input: "disabled:opacity-50 desktop:layout(px6 py3)", 72 | expected: [ 73 | { 74 | type: "variant", 75 | selector: "disabled", 76 | payload: { 77 | type: "token", 78 | value: "opacity-50" 79 | }, 80 | simple: true 81 | }, 82 | { 83 | type: "variant", 84 | selector: "desktop", 85 | payload: { 86 | type: "call", 87 | name: "layout", 88 | args: [ 89 | { 90 | type: "token", 91 | value: "px6" 92 | }, 93 | { 94 | type: "token", 95 | value: "py3" 96 | } 97 | ] 98 | }, 99 | simple: true 100 | } 101 | ] 102 | }, 103 | { 104 | input: "[.dark &[type='password']]:bg-black", 105 | expected: [ 106 | { 107 | type: "variant", 108 | selector: [ 109 | { 110 | type: "token", 111 | value: ".dark" 112 | }, 113 | { 114 | type: "token", 115 | value: "&[type='password']" 116 | } 117 | ], 118 | payload: { 119 | type: "token", 120 | value: "bg-black" 121 | } 122 | } 123 | ] 124 | }, 125 | { 126 | input: `[&:has(.desc[title="a[b] c"])]:text(underline)`, 127 | expected: [ 128 | { 129 | type: "variant", 130 | selector: [ 131 | { 132 | type: "token", 133 | value: '&:has(.desc[title="a[b] c"])' 134 | } 135 | ], 136 | payload: { 137 | type: "call", 138 | name: "text", 139 | args: [ 140 | { 141 | type: "token", 142 | value: "underline" 143 | } 144 | ] 145 | } 146 | } 147 | ] 148 | }, 149 | { 150 | input: ` 151 | theme( 152 | [.dark &]:text(text-white), 153 | hover:layout(bg-blue-700), 154 | ) 155 | `, 156 | expected: [ 157 | { 158 | type: "call", 159 | name: "theme", 160 | args: [ 161 | { 162 | type: "variant", 163 | selector: [ 164 | { 165 | type: "token", 166 | value: ".dark" 167 | }, 168 | { 169 | type: "token", 170 | value: "&" 171 | } 172 | ], 173 | payload: { 174 | type: "call", 175 | name: "text", 176 | args: [ 177 | { 178 | type: "token", 179 | value: "text-white" 180 | } 181 | ] 182 | } 183 | }, 184 | { 185 | type: "variant", 186 | selector: "hover", 187 | payload: { 188 | type: "call", 189 | name: "layout", 190 | args: [ 191 | { 192 | type: "token", 193 | value: "bg-blue-700" 194 | } 195 | ] 196 | }, 197 | simple: true 198 | } 199 | ] 200 | } 201 | ] 202 | } 203 | ]; 204 | // testing the parser 205 | for (let i = 0; i < parserCases.length; i++) { 206 | const testCase = parserCases[i]; 207 | const result = toAST(testCase.input); 208 | if (JSON.stringify(result) !== JSON.stringify(testCase.expected)) { 209 | console.error(`#${i} Test failed for input:`, testCase.input); 210 | console.error("Expected:", testCase.expected); 211 | console.error("Got :", JSON.stringify(result, null, 2)); 212 | return false; 213 | } 214 | } 215 | 216 | // testing the compiler 217 | const compilerCases = [ 218 | { 219 | usage: ["hover:mt1 fz2 active:mt1,fz2,fz3"], 220 | classStr: ["hover_mt1 fz2 active_mt1 active_fz2 active_fz3"], 221 | expectedCSS: 222 | ".hover_mt1:hover{foo:bar}.active_mt1:active{foo:bar}.active_fz2:active{foo:bar}.active_fz3:active{foo:bar}" 223 | }, 224 | { 225 | usage: ["desktop:mt1 fz2 desktop:p1", "mt2 pt1 desktop:mt1,fz3 fz2", "mobile:br-l"], 226 | classStr: ["desktop_mt1 fz2 desktop_p1", "mt2 pt1 desktop_mt1 desktop_fz3 fz2", "mobile_br-l"], 227 | expectedCSS: 228 | "@media all and (min-width:1024px){.desktop_mt1{foo:bar}.desktop_p1{foo:bar}.desktop_fz3{foo:bar}}@media all and (max-width:1023px){.mobile_br-l{foo:bar}}" 229 | }, 230 | { 231 | usage: ["[&:hover]:red,fz2 mt1", "[.dark &]:b"], 232 | classStr: ["I-hover_red I-hover_fz2 mt1", "dark-I_b"], 233 | expectedCSS: ".I-hover_red:hover{foo:bar}.I-hover_fz2:hover{foo:bar}.dark .dark-I_b{foo:bar}" 234 | }, 235 | { 236 | usage: ["desktop:[.dark &]:b desktop:mt1,p1"], 237 | classStr: ["desktop-dark-I_b desktop_mt1 desktop_p1"], 238 | expectedCSS: "@media all and (min-width:1024px){.dark .dark-I_b{foo:bar}.desktop_mt1{foo:bar}.desktop_p1{foo:bar}}" 239 | } 240 | ]; 241 | for (let i=0; i { 258 | if (fx(usage) !== testCase.classStr[i]) { 259 | console.error(`#${i} Compiler Test failed (classStr):`); 260 | console.error("Expected:\n", testCase.classStr[i]); 261 | console.error("Got:\n", fx(usage)); 262 | return false; 263 | } 264 | return true; 265 | })) { 266 | return false; 267 | } 268 | if (css !== testCase.expectedCSS) { 269 | console.error(`#${i} Compiler Test failed (expectedCSS):`); 270 | console.error("Expected:\n", testCase.expectedCSS); 271 | console.error("Got:\n", css); 272 | return false; 273 | } 274 | } 275 | return true; 276 | } -------------------------------------------------------------------------------- /examples/react/ast.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Module", 3 | "span": { 4 | "start": 2445, 5 | "end": 2674 6 | }, 7 | "body": [ 8 | { 9 | "type": "ImportDeclaration", 10 | "span": { 11 | "start": 2445, 12 | "end": 2479 13 | }, 14 | "specifiers": [ 15 | { 16 | "type": "ImportSpecifier", 17 | "span": { 18 | "start": 2454, 19 | "end": 2464 20 | }, 21 | "local": { 22 | "type": "Identifier", 23 | "span": { 24 | "start": 2454, 25 | "end": 2464 26 | }, 27 | "ctxt": 2, 28 | "value": "StrictMode", 29 | "optional": false 30 | }, 31 | "imported": null, 32 | "isTypeOnly": false 33 | } 34 | ], 35 | "source": { 36 | "type": "StringLiteral", 37 | "span": { 38 | "start": 2472, 39 | "end": 2479 40 | }, 41 | "value": "react", 42 | "raw": "'react'" 43 | }, 44 | "typeOnly": false, 45 | "with": null, 46 | "phase": "evaluation" 47 | }, 48 | { 49 | "type": "ImportDeclaration", 50 | "span": { 51 | "start": 2480, 52 | "end": 2525 53 | }, 54 | "specifiers": [ 55 | { 56 | "type": "ImportSpecifier", 57 | "span": { 58 | "start": 2489, 59 | "end": 2499 60 | }, 61 | "local": { 62 | "type": "Identifier", 63 | "span": { 64 | "start": 2489, 65 | "end": 2499 66 | }, 67 | "ctxt": 2, 68 | "value": "createRoot", 69 | "optional": false 70 | }, 71 | "imported": null, 72 | "isTypeOnly": false 73 | } 74 | ], 75 | "source": { 76 | "type": "StringLiteral", 77 | "span": { 78 | "start": 2507, 79 | "end": 2525 80 | }, 81 | "value": "react-dom/client", 82 | "raw": "'react-dom/client'" 83 | }, 84 | "typeOnly": false, 85 | "with": null, 86 | "phase": "evaluation" 87 | }, 88 | { 89 | "type": "ImportDeclaration", 90 | "span": { 91 | "start": 2526, 92 | "end": 2546 93 | }, 94 | "specifiers": [], 95 | "source": { 96 | "type": "StringLiteral", 97 | "span": { 98 | "start": 2533, 99 | "end": 2546 100 | }, 101 | "value": "./index.css", 102 | "raw": "'./index.css'" 103 | }, 104 | "typeOnly": false, 105 | "with": null, 106 | "phase": "evaluation" 107 | }, 108 | { 109 | "type": "ImportDeclaration", 110 | "span": { 111 | "start": 2547, 112 | "end": 2574 113 | }, 114 | "specifiers": [ 115 | { 116 | "type": "ImportDefaultSpecifier", 117 | "span": { 118 | "start": 2554, 119 | "end": 2557 120 | }, 121 | "local": { 122 | "type": "Identifier", 123 | "span": { 124 | "start": 2554, 125 | "end": 2557 126 | }, 127 | "ctxt": 2, 128 | "value": "App", 129 | "optional": false 130 | } 131 | } 132 | ], 133 | "source": { 134 | "type": "StringLiteral", 135 | "span": { 136 | "start": 2563, 137 | "end": 2574 138 | }, 139 | "value": "./App.tsx", 140 | "raw": "'./App.tsx'" 141 | }, 142 | "typeOnly": false, 143 | "with": null, 144 | "phase": "evaluation" 145 | }, 146 | { 147 | "type": "ExpressionStatement", 148 | "span": { 149 | "start": 2576, 150 | "end": 2674 151 | }, 152 | "expression": { 153 | "type": "CallExpression", 154 | "span": { 155 | "start": 2576, 156 | "end": 2674 157 | }, 158 | "ctxt": 0, 159 | "callee": { 160 | "type": "MemberExpression", 161 | "span": { 162 | "start": 2576, 163 | "end": 2627 164 | }, 165 | "object": { 166 | "type": "CallExpression", 167 | "span": { 168 | "start": 2576, 169 | "end": 2620 170 | }, 171 | "ctxt": 0, 172 | "callee": { 173 | "type": "Identifier", 174 | "span": { 175 | "start": 2576, 176 | "end": 2586 177 | }, 178 | "ctxt": 2, 179 | "value": "createRoot", 180 | "optional": false 181 | }, 182 | "arguments": [ 183 | { 184 | "spread": null, 185 | "expression": { 186 | "type": "TsNonNullExpression", 187 | "span": { 188 | "start": 2587, 189 | "end": 2619 190 | }, 191 | "expression": { 192 | "type": "CallExpression", 193 | "span": { 194 | "start": 2587, 195 | "end": 2618 196 | }, 197 | "ctxt": 0, 198 | "callee": { 199 | "type": "MemberExpression", 200 | "span": { 201 | "start": 2587, 202 | "end": 2610 203 | }, 204 | "object": { 205 | "type": "Identifier", 206 | "span": { 207 | "start": 2587, 208 | "end": 2595 209 | }, 210 | "ctxt": 1, 211 | "value": "document", 212 | "optional": false 213 | }, 214 | "property": { 215 | "type": "Identifier", 216 | "span": { 217 | "start": 2596, 218 | "end": 2610 219 | }, 220 | "value": "getElementById" 221 | } 222 | }, 223 | "arguments": [ 224 | { 225 | "spread": null, 226 | "expression": { 227 | "type": "StringLiteral", 228 | "span": { 229 | "start": 2611, 230 | "end": 2617 231 | }, 232 | "value": "root", 233 | "raw": "'root'" 234 | } 235 | } 236 | ], 237 | "typeArguments": null 238 | } 239 | } 240 | } 241 | ], 242 | "typeArguments": null 243 | }, 244 | "property": { 245 | "type": "Identifier", 246 | "span": { 247 | "start": 2621, 248 | "end": 2627 249 | }, 250 | "value": "render" 251 | } 252 | }, 253 | "arguments": [ 254 | { 255 | "spread": null, 256 | "expression": { 257 | "type": "JSXElement", 258 | "span": { 259 | "start": 2631, 260 | "end": 2671 261 | }, 262 | "opening": { 263 | "type": "JSXOpeningElement", 264 | "name": { 265 | "type": "Identifier", 266 | "span": { 267 | "start": 2632, 268 | "end": 2642 269 | }, 270 | "ctxt": 2, 271 | "value": "StrictMode", 272 | "optional": false 273 | }, 274 | "span": { 275 | "start": 2631, 276 | "end": 2643 277 | }, 278 | "attributes": [], 279 | "selfClosing": false, 280 | "typeArguments": null 281 | }, 282 | "children": [ 283 | { 284 | "type": "JSXText", 285 | "span": { 286 | "start": 2643, 287 | "end": 2648 288 | }, 289 | "value": "\n ", 290 | "raw": "\n " 291 | }, 292 | { 293 | "type": "JSXElement", 294 | "span": { 295 | "start": 2648, 296 | "end": 2655 297 | }, 298 | "opening": { 299 | "type": "JSXOpeningElement", 300 | "name": { 301 | "type": "Identifier", 302 | "span": { 303 | "start": 2649, 304 | "end": 2652 305 | }, 306 | "ctxt": 2, 307 | "value": "App", 308 | "optional": false 309 | }, 310 | "span": { 311 | "start": 2648, 312 | "end": 2655 313 | }, 314 | "attributes": [], 315 | "selfClosing": true, 316 | "typeArguments": null 317 | }, 318 | "children": [], 319 | "closing": null 320 | }, 321 | { 322 | "type": "JSXText", 323 | "span": { 324 | "start": 2655, 325 | "end": 2658 326 | }, 327 | "value": "\n ", 328 | "raw": "\n " 329 | } 330 | ], 331 | "closing": { 332 | "type": "JSXClosingElement", 333 | "span": { 334 | "start": 2658, 335 | "end": 2671 336 | }, 337 | "name": { 338 | "type": "Identifier", 339 | "span": { 340 | "start": 2660, 341 | "end": 2670 342 | }, 343 | "ctxt": 2, 344 | "value": "StrictMode", 345 | "optional": false 346 | } 347 | } 348 | } 349 | } 350 | ], 351 | "typeArguments": null 352 | } 353 | } 354 | ], 355 | "interpreter": null 356 | } -------------------------------------------------------------------------------- /packages/site/public/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.30.0 2 | https://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+jsx+tsx+typescript */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(Cg.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 8 | !function(t){var n=t.util.clone(t.languages.javascript),e="(?:\\{*\\.{3}(?:[^{}]|)*\\})";function a(t,n){return t=t.replace(//g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(//g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(//g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend("markup",n),t.languages.jsx.tag.pattern=a("+(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+|))?|))**/?)?>"),t.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,t.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,t.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore("inside","attr-name",{spread:{pattern:a(""),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("="),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?"string"==typeof t?t:"string"==typeof t.content?t.content:t.content.map(s).join(""):""},g=function(n){for(var e=[],a=0;a0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():"/>"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&"punctuation"===o.type&&"{"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&"punctuation"===o.type&&"}"===o.content?e[e.length-1].openedBraces--:i=!0),(i||"string"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a0&&("string"==typeof n[a-1]||"plain-text"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token("plain-text",r,null,r)}o.content&&"string"!=typeof o.content&&g(o.content)}};t.hooks.add("after-tokenize",(function(t){"jsx"!==t.language&&"tsx"!==t.language||g(t.tokens)}))}(Prism); 9 | !function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism); 10 | !function(e){var a=e.util.clone(e.languages.typescript);e.languages.tsx=e.languages.extend("jsx",a),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"];var t=e.languages.tsx.tag;t.pattern=RegExp("(^|[^\\w$]|(?== 0.6" 22 | } 23 | }, 24 | "node_modules/body-parser": { 25 | "version": "2.2.1", 26 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", 27 | "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", 28 | "license": "MIT", 29 | "dependencies": { 30 | "bytes": "^3.1.2", 31 | "content-type": "^1.0.5", 32 | "debug": "^4.4.3", 33 | "http-errors": "^2.0.0", 34 | "iconv-lite": "^0.7.0", 35 | "on-finished": "^2.4.1", 36 | "qs": "^6.14.0", 37 | "raw-body": "^3.0.1", 38 | "type-is": "^2.0.1" 39 | }, 40 | "engines": { 41 | "node": ">=18" 42 | }, 43 | "funding": { 44 | "type": "opencollective", 45 | "url": "https://opencollective.com/express" 46 | } 47 | }, 48 | "node_modules/bytes": { 49 | "version": "3.1.2", 50 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 51 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 52 | "license": "MIT", 53 | "engines": { 54 | "node": ">= 0.8" 55 | } 56 | }, 57 | "node_modules/call-bind-apply-helpers": { 58 | "version": "1.0.2", 59 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 60 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 61 | "license": "MIT", 62 | "dependencies": { 63 | "es-errors": "^1.3.0", 64 | "function-bind": "^1.1.2" 65 | }, 66 | "engines": { 67 | "node": ">= 0.4" 68 | } 69 | }, 70 | "node_modules/call-bound": { 71 | "version": "1.0.4", 72 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 73 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 74 | "license": "MIT", 75 | "dependencies": { 76 | "call-bind-apply-helpers": "^1.0.2", 77 | "get-intrinsic": "^1.3.0" 78 | }, 79 | "engines": { 80 | "node": ">= 0.4" 81 | }, 82 | "funding": { 83 | "url": "https://github.com/sponsors/ljharb" 84 | } 85 | }, 86 | "node_modules/content-disposition": { 87 | "version": "1.0.1", 88 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", 89 | "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", 90 | "license": "MIT", 91 | "engines": { 92 | "node": ">=18" 93 | }, 94 | "funding": { 95 | "type": "opencollective", 96 | "url": "https://opencollective.com/express" 97 | } 98 | }, 99 | "node_modules/content-type": { 100 | "version": "1.0.5", 101 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 102 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 103 | "license": "MIT", 104 | "engines": { 105 | "node": ">= 0.6" 106 | } 107 | }, 108 | "node_modules/cookie": { 109 | "version": "0.7.2", 110 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 111 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 112 | "license": "MIT", 113 | "engines": { 114 | "node": ">= 0.6" 115 | } 116 | }, 117 | "node_modules/cookie-signature": { 118 | "version": "1.2.2", 119 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 120 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 121 | "license": "MIT", 122 | "engines": { 123 | "node": ">=6.6.0" 124 | } 125 | }, 126 | "node_modules/debug": { 127 | "version": "4.4.3", 128 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 129 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 130 | "license": "MIT", 131 | "dependencies": { 132 | "ms": "^2.1.3" 133 | }, 134 | "engines": { 135 | "node": ">=6.0" 136 | }, 137 | "peerDependenciesMeta": { 138 | "supports-color": { 139 | "optional": true 140 | } 141 | } 142 | }, 143 | "node_modules/depd": { 144 | "version": "2.0.0", 145 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 146 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 147 | "license": "MIT", 148 | "engines": { 149 | "node": ">= 0.8" 150 | } 151 | }, 152 | "node_modules/dunder-proto": { 153 | "version": "1.0.1", 154 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 155 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 156 | "license": "MIT", 157 | "dependencies": { 158 | "call-bind-apply-helpers": "^1.0.1", 159 | "es-errors": "^1.3.0", 160 | "gopd": "^1.2.0" 161 | }, 162 | "engines": { 163 | "node": ">= 0.4" 164 | } 165 | }, 166 | "node_modules/ee-first": { 167 | "version": "1.1.1", 168 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 169 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 170 | "license": "MIT" 171 | }, 172 | "node_modules/encodeurl": { 173 | "version": "2.0.0", 174 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 175 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 176 | "license": "MIT", 177 | "engines": { 178 | "node": ">= 0.8" 179 | } 180 | }, 181 | "node_modules/es-define-property": { 182 | "version": "1.0.1", 183 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 184 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 185 | "license": "MIT", 186 | "engines": { 187 | "node": ">= 0.4" 188 | } 189 | }, 190 | "node_modules/es-errors": { 191 | "version": "1.3.0", 192 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 193 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 194 | "license": "MIT", 195 | "engines": { 196 | "node": ">= 0.4" 197 | } 198 | }, 199 | "node_modules/es-object-atoms": { 200 | "version": "1.1.1", 201 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 202 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 203 | "license": "MIT", 204 | "dependencies": { 205 | "es-errors": "^1.3.0" 206 | }, 207 | "engines": { 208 | "node": ">= 0.4" 209 | } 210 | }, 211 | "node_modules/escape-html": { 212 | "version": "1.0.3", 213 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 214 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 215 | "license": "MIT" 216 | }, 217 | "node_modules/etag": { 218 | "version": "1.8.1", 219 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 220 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 221 | "license": "MIT", 222 | "engines": { 223 | "node": ">= 0.6" 224 | } 225 | }, 226 | "node_modules/express": { 227 | "version": "5.2.1", 228 | "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", 229 | "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", 230 | "license": "MIT", 231 | "dependencies": { 232 | "accepts": "^2.0.0", 233 | "body-parser": "^2.2.1", 234 | "content-disposition": "^1.0.0", 235 | "content-type": "^1.0.5", 236 | "cookie": "^0.7.1", 237 | "cookie-signature": "^1.2.1", 238 | "debug": "^4.4.0", 239 | "depd": "^2.0.0", 240 | "encodeurl": "^2.0.0", 241 | "escape-html": "^1.0.3", 242 | "etag": "^1.8.1", 243 | "finalhandler": "^2.1.0", 244 | "fresh": "^2.0.0", 245 | "http-errors": "^2.0.0", 246 | "merge-descriptors": "^2.0.0", 247 | "mime-types": "^3.0.0", 248 | "on-finished": "^2.4.1", 249 | "once": "^1.4.0", 250 | "parseurl": "^1.3.3", 251 | "proxy-addr": "^2.0.7", 252 | "qs": "^6.14.0", 253 | "range-parser": "^1.2.1", 254 | "router": "^2.2.0", 255 | "send": "^1.1.0", 256 | "serve-static": "^2.2.0", 257 | "statuses": "^2.0.1", 258 | "type-is": "^2.0.1", 259 | "vary": "^1.1.2" 260 | }, 261 | "engines": { 262 | "node": ">= 18" 263 | }, 264 | "funding": { 265 | "type": "opencollective", 266 | "url": "https://opencollective.com/express" 267 | } 268 | }, 269 | "node_modules/finalhandler": { 270 | "version": "2.1.1", 271 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", 272 | "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", 273 | "license": "MIT", 274 | "dependencies": { 275 | "debug": "^4.4.0", 276 | "encodeurl": "^2.0.0", 277 | "escape-html": "^1.0.3", 278 | "on-finished": "^2.4.1", 279 | "parseurl": "^1.3.3", 280 | "statuses": "^2.0.1" 281 | }, 282 | "engines": { 283 | "node": ">= 18.0.0" 284 | }, 285 | "funding": { 286 | "type": "opencollective", 287 | "url": "https://opencollective.com/express" 288 | } 289 | }, 290 | "node_modules/forwarded": { 291 | "version": "0.2.0", 292 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 293 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 294 | "license": "MIT", 295 | "engines": { 296 | "node": ">= 0.6" 297 | } 298 | }, 299 | "node_modules/fresh": { 300 | "version": "2.0.0", 301 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 302 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 303 | "license": "MIT", 304 | "engines": { 305 | "node": ">= 0.8" 306 | } 307 | }, 308 | "node_modules/function-bind": { 309 | "version": "1.1.2", 310 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 311 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 312 | "license": "MIT", 313 | "funding": { 314 | "url": "https://github.com/sponsors/ljharb" 315 | } 316 | }, 317 | "node_modules/get-intrinsic": { 318 | "version": "1.3.0", 319 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 320 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 321 | "license": "MIT", 322 | "dependencies": { 323 | "call-bind-apply-helpers": "^1.0.2", 324 | "es-define-property": "^1.0.1", 325 | "es-errors": "^1.3.0", 326 | "es-object-atoms": "^1.1.1", 327 | "function-bind": "^1.1.2", 328 | "get-proto": "^1.0.1", 329 | "gopd": "^1.2.0", 330 | "has-symbols": "^1.1.0", 331 | "hasown": "^2.0.2", 332 | "math-intrinsics": "^1.1.0" 333 | }, 334 | "engines": { 335 | "node": ">= 0.4" 336 | }, 337 | "funding": { 338 | "url": "https://github.com/sponsors/ljharb" 339 | } 340 | }, 341 | "node_modules/get-proto": { 342 | "version": "1.0.1", 343 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 344 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 345 | "license": "MIT", 346 | "dependencies": { 347 | "dunder-proto": "^1.0.1", 348 | "es-object-atoms": "^1.0.0" 349 | }, 350 | "engines": { 351 | "node": ">= 0.4" 352 | } 353 | }, 354 | "node_modules/gopd": { 355 | "version": "1.2.0", 356 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 357 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 358 | "license": "MIT", 359 | "engines": { 360 | "node": ">= 0.4" 361 | }, 362 | "funding": { 363 | "url": "https://github.com/sponsors/ljharb" 364 | } 365 | }, 366 | "node_modules/has-symbols": { 367 | "version": "1.1.0", 368 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 369 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 370 | "license": "MIT", 371 | "engines": { 372 | "node": ">= 0.4" 373 | }, 374 | "funding": { 375 | "url": "https://github.com/sponsors/ljharb" 376 | } 377 | }, 378 | "node_modules/hasown": { 379 | "version": "2.0.2", 380 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 381 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 382 | "license": "MIT", 383 | "dependencies": { 384 | "function-bind": "^1.1.2" 385 | }, 386 | "engines": { 387 | "node": ">= 0.4" 388 | } 389 | }, 390 | "node_modules/http-errors": { 391 | "version": "2.0.1", 392 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", 393 | "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", 394 | "license": "MIT", 395 | "dependencies": { 396 | "depd": "~2.0.0", 397 | "inherits": "~2.0.4", 398 | "setprototypeof": "~1.2.0", 399 | "statuses": "~2.0.2", 400 | "toidentifier": "~1.0.1" 401 | }, 402 | "engines": { 403 | "node": ">= 0.8" 404 | }, 405 | "funding": { 406 | "type": "opencollective", 407 | "url": "https://opencollective.com/express" 408 | } 409 | }, 410 | "node_modules/iconv-lite": { 411 | "version": "0.7.0", 412 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", 413 | "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", 414 | "license": "MIT", 415 | "dependencies": { 416 | "safer-buffer": ">= 2.1.2 < 3.0.0" 417 | }, 418 | "engines": { 419 | "node": ">=0.10.0" 420 | }, 421 | "funding": { 422 | "type": "opencollective", 423 | "url": "https://opencollective.com/express" 424 | } 425 | }, 426 | "node_modules/inherits": { 427 | "version": "2.0.4", 428 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 429 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 430 | "license": "ISC" 431 | }, 432 | "node_modules/ipaddr.js": { 433 | "version": "1.9.1", 434 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 435 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 436 | "license": "MIT", 437 | "engines": { 438 | "node": ">= 0.10" 439 | } 440 | }, 441 | "node_modules/is-promise": { 442 | "version": "4.0.0", 443 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 444 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 445 | "license": "MIT" 446 | }, 447 | "node_modules/math-intrinsics": { 448 | "version": "1.1.0", 449 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 450 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 451 | "license": "MIT", 452 | "engines": { 453 | "node": ">= 0.4" 454 | } 455 | }, 456 | "node_modules/media-typer": { 457 | "version": "1.1.0", 458 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 459 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 460 | "license": "MIT", 461 | "engines": { 462 | "node": ">= 0.8" 463 | } 464 | }, 465 | "node_modules/merge-descriptors": { 466 | "version": "2.0.0", 467 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 468 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 469 | "license": "MIT", 470 | "engines": { 471 | "node": ">=18" 472 | }, 473 | "funding": { 474 | "url": "https://github.com/sponsors/sindresorhus" 475 | } 476 | }, 477 | "node_modules/mime-db": { 478 | "version": "1.54.0", 479 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 480 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 481 | "license": "MIT", 482 | "engines": { 483 | "node": ">= 0.6" 484 | } 485 | }, 486 | "node_modules/mime-types": { 487 | "version": "3.0.2", 488 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 489 | "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 490 | "license": "MIT", 491 | "dependencies": { 492 | "mime-db": "^1.54.0" 493 | }, 494 | "engines": { 495 | "node": ">=18" 496 | }, 497 | "funding": { 498 | "type": "opencollective", 499 | "url": "https://opencollective.com/express" 500 | } 501 | }, 502 | "node_modules/ms": { 503 | "version": "2.1.3", 504 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 505 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 506 | "license": "MIT" 507 | }, 508 | "node_modules/negotiator": { 509 | "version": "1.0.0", 510 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 511 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 512 | "license": "MIT", 513 | "engines": { 514 | "node": ">= 0.6" 515 | } 516 | }, 517 | "node_modules/object-inspect": { 518 | "version": "1.13.4", 519 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 520 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 521 | "license": "MIT", 522 | "engines": { 523 | "node": ">= 0.4" 524 | }, 525 | "funding": { 526 | "url": "https://github.com/sponsors/ljharb" 527 | } 528 | }, 529 | "node_modules/on-finished": { 530 | "version": "2.4.1", 531 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 532 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 533 | "license": "MIT", 534 | "dependencies": { 535 | "ee-first": "1.1.1" 536 | }, 537 | "engines": { 538 | "node": ">= 0.8" 539 | } 540 | }, 541 | "node_modules/once": { 542 | "version": "1.4.0", 543 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 544 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 545 | "license": "ISC", 546 | "dependencies": { 547 | "wrappy": "1" 548 | } 549 | }, 550 | "node_modules/parseurl": { 551 | "version": "1.3.3", 552 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 553 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 554 | "license": "MIT", 555 | "engines": { 556 | "node": ">= 0.8" 557 | } 558 | }, 559 | "node_modules/path-to-regexp": { 560 | "version": "8.3.0", 561 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", 562 | "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", 563 | "license": "MIT", 564 | "funding": { 565 | "type": "opencollective", 566 | "url": "https://opencollective.com/express" 567 | } 568 | }, 569 | "node_modules/proxy-addr": { 570 | "version": "2.0.7", 571 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 572 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 573 | "license": "MIT", 574 | "dependencies": { 575 | "forwarded": "0.2.0", 576 | "ipaddr.js": "1.9.1" 577 | }, 578 | "engines": { 579 | "node": ">= 0.10" 580 | } 581 | }, 582 | "node_modules/qs": { 583 | "version": "6.14.0", 584 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 585 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 586 | "license": "BSD-3-Clause", 587 | "dependencies": { 588 | "side-channel": "^1.1.0" 589 | }, 590 | "engines": { 591 | "node": ">=0.6" 592 | }, 593 | "funding": { 594 | "url": "https://github.com/sponsors/ljharb" 595 | } 596 | }, 597 | "node_modules/range-parser": { 598 | "version": "1.2.1", 599 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 600 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 601 | "license": "MIT", 602 | "engines": { 603 | "node": ">= 0.6" 604 | } 605 | }, 606 | "node_modules/raw-body": { 607 | "version": "3.0.2", 608 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", 609 | "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", 610 | "license": "MIT", 611 | "dependencies": { 612 | "bytes": "~3.1.2", 613 | "http-errors": "~2.0.1", 614 | "iconv-lite": "~0.7.0", 615 | "unpipe": "~1.0.0" 616 | }, 617 | "engines": { 618 | "node": ">= 0.10" 619 | } 620 | }, 621 | "node_modules/router": { 622 | "version": "2.2.0", 623 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 624 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 625 | "license": "MIT", 626 | "dependencies": { 627 | "debug": "^4.4.0", 628 | "depd": "^2.0.0", 629 | "is-promise": "^4.0.0", 630 | "parseurl": "^1.3.3", 631 | "path-to-regexp": "^8.0.0" 632 | }, 633 | "engines": { 634 | "node": ">= 18" 635 | } 636 | }, 637 | "node_modules/safer-buffer": { 638 | "version": "2.1.2", 639 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 640 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 641 | "license": "MIT" 642 | }, 643 | "node_modules/send": { 644 | "version": "1.2.0", 645 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 646 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 647 | "license": "MIT", 648 | "dependencies": { 649 | "debug": "^4.3.5", 650 | "encodeurl": "^2.0.0", 651 | "escape-html": "^1.0.3", 652 | "etag": "^1.8.1", 653 | "fresh": "^2.0.0", 654 | "http-errors": "^2.0.0", 655 | "mime-types": "^3.0.1", 656 | "ms": "^2.1.3", 657 | "on-finished": "^2.4.1", 658 | "range-parser": "^1.2.1", 659 | "statuses": "^2.0.1" 660 | }, 661 | "engines": { 662 | "node": ">= 18" 663 | } 664 | }, 665 | "node_modules/serve-static": { 666 | "version": "2.2.0", 667 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 668 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 669 | "license": "MIT", 670 | "dependencies": { 671 | "encodeurl": "^2.0.0", 672 | "escape-html": "^1.0.3", 673 | "parseurl": "^1.3.3", 674 | "send": "^1.2.0" 675 | }, 676 | "engines": { 677 | "node": ">= 18" 678 | } 679 | }, 680 | "node_modules/setprototypeof": { 681 | "version": "1.2.0", 682 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 683 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 684 | "license": "ISC" 685 | }, 686 | "node_modules/side-channel": { 687 | "version": "1.1.0", 688 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 689 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 690 | "license": "MIT", 691 | "dependencies": { 692 | "es-errors": "^1.3.0", 693 | "object-inspect": "^1.13.3", 694 | "side-channel-list": "^1.0.0", 695 | "side-channel-map": "^1.0.1", 696 | "side-channel-weakmap": "^1.0.2" 697 | }, 698 | "engines": { 699 | "node": ">= 0.4" 700 | }, 701 | "funding": { 702 | "url": "https://github.com/sponsors/ljharb" 703 | } 704 | }, 705 | "node_modules/side-channel-list": { 706 | "version": "1.0.0", 707 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 708 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 709 | "license": "MIT", 710 | "dependencies": { 711 | "es-errors": "^1.3.0", 712 | "object-inspect": "^1.13.3" 713 | }, 714 | "engines": { 715 | "node": ">= 0.4" 716 | }, 717 | "funding": { 718 | "url": "https://github.com/sponsors/ljharb" 719 | } 720 | }, 721 | "node_modules/side-channel-map": { 722 | "version": "1.0.1", 723 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 724 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 725 | "license": "MIT", 726 | "dependencies": { 727 | "call-bound": "^1.0.2", 728 | "es-errors": "^1.3.0", 729 | "get-intrinsic": "^1.2.5", 730 | "object-inspect": "^1.13.3" 731 | }, 732 | "engines": { 733 | "node": ">= 0.4" 734 | }, 735 | "funding": { 736 | "url": "https://github.com/sponsors/ljharb" 737 | } 738 | }, 739 | "node_modules/side-channel-weakmap": { 740 | "version": "1.0.2", 741 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 742 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 743 | "license": "MIT", 744 | "dependencies": { 745 | "call-bound": "^1.0.2", 746 | "es-errors": "^1.3.0", 747 | "get-intrinsic": "^1.2.5", 748 | "object-inspect": "^1.13.3", 749 | "side-channel-map": "^1.0.1" 750 | }, 751 | "engines": { 752 | "node": ">= 0.4" 753 | }, 754 | "funding": { 755 | "url": "https://github.com/sponsors/ljharb" 756 | } 757 | }, 758 | "node_modules/statuses": { 759 | "version": "2.0.2", 760 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", 761 | "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", 762 | "license": "MIT", 763 | "engines": { 764 | "node": ">= 0.8" 765 | } 766 | }, 767 | "node_modules/toidentifier": { 768 | "version": "1.0.1", 769 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 770 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 771 | "license": "MIT", 772 | "engines": { 773 | "node": ">=0.6" 774 | } 775 | }, 776 | "node_modules/type-is": { 777 | "version": "2.0.1", 778 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 779 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 780 | "license": "MIT", 781 | "dependencies": { 782 | "content-type": "^1.0.5", 783 | "media-typer": "^1.1.0", 784 | "mime-types": "^3.0.0" 785 | }, 786 | "engines": { 787 | "node": ">= 0.6" 788 | } 789 | }, 790 | "node_modules/unpipe": { 791 | "version": "1.0.0", 792 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 793 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 794 | "license": "MIT", 795 | "engines": { 796 | "node": ">= 0.8" 797 | } 798 | }, 799 | "node_modules/vary": { 800 | "version": "1.1.2", 801 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 802 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 803 | "license": "MIT", 804 | "engines": { 805 | "node": ">= 0.8" 806 | } 807 | }, 808 | "node_modules/wrappy": { 809 | "version": "1.0.2", 810 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 811 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 812 | "license": "ISC" 813 | } 814 | } 815 | } 816 | --------------------------------------------------------------------------------