@luna_ui/luna 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/{index-CyEkcO3_.d.ts → index-CDWzWF-h.d.ts} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-DEjrAhrg.js +1 -0
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/hydration/createHydrator.ts +62 -0
- package/src/hydration/delegate.ts +62 -0
- package/src/hydration/drag.ts +214 -0
- package/src/hydration/index.ts +12 -0
- package/src/hydration/keyboard.ts +64 -0
- package/src/hydration/toggle.ts +101 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
- package/dist/src-BDdxGwvq.js +0 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Co-occurrence analysis for CSS classes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClassUsage, CoOccurrence } from "./types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a co-occurrence matrix from class usages
|
|
9
|
+
* Returns a nested map: classA -> classB -> count
|
|
10
|
+
* Classes are stored in alphabetical order (A < B)
|
|
11
|
+
*/
|
|
12
|
+
export function buildCooccurrenceMatrix(
|
|
13
|
+
usages: ClassUsage[]
|
|
14
|
+
): Map<string, Map<string, number>> {
|
|
15
|
+
const matrix = new Map<string, Map<string, number>>();
|
|
16
|
+
|
|
17
|
+
for (const usage of usages) {
|
|
18
|
+
const classes = usage.classes;
|
|
19
|
+
const n = classes.length;
|
|
20
|
+
|
|
21
|
+
// Count all pairs (i, j) where i < j
|
|
22
|
+
for (let i = 0; i < n; i++) {
|
|
23
|
+
for (let j = i + 1; j < n; j++) {
|
|
24
|
+
const a = classes[i];
|
|
25
|
+
const b = classes[j];
|
|
26
|
+
|
|
27
|
+
// Ensure consistent ordering (a < b alphabetically)
|
|
28
|
+
const [first, second] = a < b ? [a, b] : [b, a];
|
|
29
|
+
|
|
30
|
+
if (!matrix.has(first)) {
|
|
31
|
+
matrix.set(first, new Map());
|
|
32
|
+
}
|
|
33
|
+
const inner = matrix.get(first)!;
|
|
34
|
+
inner.set(second, (inner.get(second) || 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return matrix;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert matrix to a flat array of co-occurrences
|
|
44
|
+
* Sorted by frequency descending
|
|
45
|
+
*/
|
|
46
|
+
export function matrixToCooccurrences(
|
|
47
|
+
matrix: Map<string, Map<string, number>>
|
|
48
|
+
): CoOccurrence[] {
|
|
49
|
+
const result: CoOccurrence[] = [];
|
|
50
|
+
|
|
51
|
+
for (const [classA, inner] of matrix) {
|
|
52
|
+
for (const [classB, frequency] of inner) {
|
|
53
|
+
result.push({ classA, classB, frequency });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort by frequency descending
|
|
58
|
+
result.sort((a, b) => b.frequency - a.frequency);
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the top N co-occurring pairs
|
|
65
|
+
*/
|
|
66
|
+
export function getTopCooccurrences(
|
|
67
|
+
cooccurrences: CoOccurrence[],
|
|
68
|
+
n: number
|
|
69
|
+
): CoOccurrence[] {
|
|
70
|
+
return cooccurrences.slice(0, n);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build an adjacency list for graph algorithms
|
|
75
|
+
* Only includes pairs with frequency >= minFrequency
|
|
76
|
+
*/
|
|
77
|
+
export function buildAdjacencyList(
|
|
78
|
+
cooccurrences: CoOccurrence[],
|
|
79
|
+
minFrequency: number
|
|
80
|
+
): Map<string, Array<{ target: string; weight: number }>> {
|
|
81
|
+
const adj = new Map<string, Array<{ target: string; weight: number }>>();
|
|
82
|
+
|
|
83
|
+
for (const co of cooccurrences) {
|
|
84
|
+
if (co.frequency >= minFrequency) {
|
|
85
|
+
// Add edge a -> b
|
|
86
|
+
if (!adj.has(co.classA)) {
|
|
87
|
+
adj.set(co.classA, []);
|
|
88
|
+
}
|
|
89
|
+
adj.get(co.classA)!.push({ target: co.classB, weight: co.frequency });
|
|
90
|
+
|
|
91
|
+
// Add edge b -> a (undirected graph)
|
|
92
|
+
if (!adj.has(co.classB)) {
|
|
93
|
+
adj.set(co.classB, []);
|
|
94
|
+
}
|
|
95
|
+
adj.get(co.classB)!.push({ target: co.classA, weight: co.frequency });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return adj;
|
|
100
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Co-occurrence Optimizer - Core Module
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic core functions for CSS class co-occurrence analysis.
|
|
5
|
+
* This module contains pure functions with no framework dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ClassUsage, MergePattern, OptimizeResult } from "./types.js";
|
|
9
|
+
import { hashMergedClassName } from "./hash.js";
|
|
10
|
+
import { findFrequentPatterns } from "./pattern.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Core optimization options (framework-agnostic)
|
|
14
|
+
*/
|
|
15
|
+
export interface CoreOptimizeOptions {
|
|
16
|
+
/** Minimum frequency for a pattern to be merged (default: 2) */
|
|
17
|
+
minFrequency?: number;
|
|
18
|
+
/** Maximum pattern size to consider (default: 5) */
|
|
19
|
+
maxPatternSize?: number;
|
|
20
|
+
/** Pretty print CSS output (default: false) */
|
|
21
|
+
pretty?: boolean;
|
|
22
|
+
/** Enable verbose logging (default: false) */
|
|
23
|
+
verbose?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Core optimization result
|
|
28
|
+
*/
|
|
29
|
+
export interface CoreOptimizeResult {
|
|
30
|
+
/** Optimized CSS string */
|
|
31
|
+
css: string;
|
|
32
|
+
/** Mapping from sorted class combination to merged class name */
|
|
33
|
+
mergeMap: Map<string, string>;
|
|
34
|
+
/** Detected patterns */
|
|
35
|
+
patterns: MergePattern[];
|
|
36
|
+
/** Statistics */
|
|
37
|
+
stats: {
|
|
38
|
+
originalClasses: number;
|
|
39
|
+
mergedPatterns: number;
|
|
40
|
+
estimatedBytesSaved: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Core optimizer - performs pattern analysis and CSS generation
|
|
46
|
+
*
|
|
47
|
+
* This is the framework-agnostic core that:
|
|
48
|
+
* 1. Analyzes class usages to find frequent patterns
|
|
49
|
+
* 2. Generates merged CSS rules
|
|
50
|
+
* 3. Returns a merge map for HTML transformation
|
|
51
|
+
*
|
|
52
|
+
* @param usages - Array of class usages extracted from source
|
|
53
|
+
* @param css - Original CSS content
|
|
54
|
+
* @param classToDeclaration - Map from class name to CSS declaration
|
|
55
|
+
* @param options - Optimization options
|
|
56
|
+
*/
|
|
57
|
+
export function optimizeCore(
|
|
58
|
+
usages: ClassUsage[],
|
|
59
|
+
css: string,
|
|
60
|
+
classToDeclaration: Map<string, string>,
|
|
61
|
+
options: CoreOptimizeOptions = {}
|
|
62
|
+
): CoreOptimizeResult {
|
|
63
|
+
const {
|
|
64
|
+
minFrequency = 2,
|
|
65
|
+
maxPatternSize = 5,
|
|
66
|
+
pretty = false,
|
|
67
|
+
verbose = false,
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
const log = (msg: string) => {
|
|
71
|
+
if (verbose) {
|
|
72
|
+
console.log(`[css-optimizer] ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Early return for empty input
|
|
77
|
+
if (usages.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
css,
|
|
80
|
+
mergeMap: new Map(),
|
|
81
|
+
patterns: [],
|
|
82
|
+
stats: {
|
|
83
|
+
originalClasses: classToDeclaration.size,
|
|
84
|
+
mergedPatterns: 0,
|
|
85
|
+
estimatedBytesSaved: 0,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log(`Analyzing ${usages.length} class usage sites`);
|
|
91
|
+
|
|
92
|
+
// Step 1: Find frequent patterns
|
|
93
|
+
const patterns = findFrequentPatterns(usages, minFrequency, maxPatternSize);
|
|
94
|
+
log(`Found ${patterns.length} frequent patterns`);
|
|
95
|
+
|
|
96
|
+
// Step 2: Generate merged classes
|
|
97
|
+
const mergeMap = new Map<string, string>();
|
|
98
|
+
const cssRules: string[] = [];
|
|
99
|
+
const usedClasses = new Set<string>();
|
|
100
|
+
let totalBytesSaved = 0;
|
|
101
|
+
|
|
102
|
+
for (const pattern of patterns) {
|
|
103
|
+
// Collect declarations for all classes in pattern
|
|
104
|
+
const declarations: string[] = [];
|
|
105
|
+
let allFound = true;
|
|
106
|
+
|
|
107
|
+
for (const cls of pattern.originalClasses) {
|
|
108
|
+
const decl = classToDeclaration.get(cls);
|
|
109
|
+
if (decl) {
|
|
110
|
+
declarations.push(decl);
|
|
111
|
+
} else {
|
|
112
|
+
allFound = false;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!allFound || declarations.length === 0) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for conflicts (class already used in another merge)
|
|
122
|
+
let hasConflict = false;
|
|
123
|
+
for (const cls of pattern.originalClasses) {
|
|
124
|
+
if (usedClasses.has(cls)) {
|
|
125
|
+
hasConflict = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (hasConflict) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Generate merged class name
|
|
135
|
+
const mergedClass = hashMergedClassName(declarations);
|
|
136
|
+
|
|
137
|
+
pattern.mergedClass = mergedClass;
|
|
138
|
+
pattern.declarations = declarations;
|
|
139
|
+
|
|
140
|
+
// Create merge key (sorted classes joined by space)
|
|
141
|
+
const mergeKey = pattern.originalClasses.join(" ");
|
|
142
|
+
mergeMap.set(mergeKey, mergedClass);
|
|
143
|
+
|
|
144
|
+
// Mark classes as used
|
|
145
|
+
for (const cls of pattern.originalClasses) {
|
|
146
|
+
usedClasses.add(cls);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Generate CSS rule
|
|
150
|
+
const declStr = declarations.join(";");
|
|
151
|
+
if (pretty) {
|
|
152
|
+
cssRules.push(`.${mergedClass} { ${declStr.replace(/;/g, "; ")} }`);
|
|
153
|
+
} else {
|
|
154
|
+
cssRules.push(`.${mergedClass}{${declStr}}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
totalBytesSaved += pattern.bytesSaved;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Step 3: Filter out merged classes from original CSS
|
|
161
|
+
const originalRules: string[] = [];
|
|
162
|
+
const rulePattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\{([^}]+)\}/g;
|
|
163
|
+
|
|
164
|
+
let match: RegExpExecArray | null;
|
|
165
|
+
rulePattern.lastIndex = 0;
|
|
166
|
+
while ((match = rulePattern.exec(css)) !== null) {
|
|
167
|
+
const cls = match[1];
|
|
168
|
+
if (!usedClasses.has(cls)) {
|
|
169
|
+
originalRules.push(match[0]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Preserve @media rules
|
|
174
|
+
const mediaPattern = /@media[^{]+\{[^}]+\}/g;
|
|
175
|
+
mediaPattern.lastIndex = 0;
|
|
176
|
+
while ((match = mediaPattern.exec(css)) !== null) {
|
|
177
|
+
originalRules.push(match[0]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Preserve pseudo-class rules
|
|
181
|
+
const pseudoPattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)(:[a-zA-Z-]+)\{([^}]+)\}/g;
|
|
182
|
+
pseudoPattern.lastIndex = 0;
|
|
183
|
+
while ((match = pseudoPattern.exec(css)) !== null) {
|
|
184
|
+
originalRules.push(match[0]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Combine merged + remaining original rules
|
|
188
|
+
const separator = pretty ? "\n" : "";
|
|
189
|
+
const optimizedCss = [...cssRules, ...originalRules].join(separator);
|
|
190
|
+
|
|
191
|
+
const actualPatterns = patterns.filter((p) => p.mergedClass !== "");
|
|
192
|
+
|
|
193
|
+
log(`Merged ${actualPatterns.length} patterns`);
|
|
194
|
+
log(`Estimated savings: ${totalBytesSaved} bytes`);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
css: optimizedCss,
|
|
198
|
+
mergeMap,
|
|
199
|
+
patterns: actualPatterns,
|
|
200
|
+
stats: {
|
|
201
|
+
originalClasses: classToDeclaration.size,
|
|
202
|
+
mergedPatterns: actualPatterns.length,
|
|
203
|
+
estimatedBytesSaved: totalBytesSaved,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Apply merge map to class list
|
|
210
|
+
*
|
|
211
|
+
* Takes a list of classes and returns the optimized list
|
|
212
|
+
* with merged classes replacing original combinations.
|
|
213
|
+
*
|
|
214
|
+
* @param classes - Array of class names
|
|
215
|
+
* @param mergeMap - Merge map from optimizeCore
|
|
216
|
+
* @param classPrefix - Prefix for classes to consider for merging
|
|
217
|
+
*/
|
|
218
|
+
export function applyMergeToClasses(
|
|
219
|
+
classes: string[],
|
|
220
|
+
mergeMap: Map<string, string>,
|
|
221
|
+
classPrefix = "_"
|
|
222
|
+
): string[] {
|
|
223
|
+
if (mergeMap.size === 0) {
|
|
224
|
+
return classes;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Separate prefixed and non-prefixed classes
|
|
228
|
+
const prefixedClasses = classes.filter((c) => c.startsWith(classPrefix));
|
|
229
|
+
const otherClasses = classes.filter((c) => !c.startsWith(classPrefix));
|
|
230
|
+
|
|
231
|
+
// Sort for consistent matching
|
|
232
|
+
prefixedClasses.sort();
|
|
233
|
+
|
|
234
|
+
let result = [...prefixedClasses];
|
|
235
|
+
const merged: string[] = [];
|
|
236
|
+
|
|
237
|
+
// Sort merge keys by length descending (longer patterns first)
|
|
238
|
+
const sortedKeys = [...mergeMap.keys()].sort((a, b) => b.length - a.length);
|
|
239
|
+
|
|
240
|
+
// Try to apply each merge pattern
|
|
241
|
+
for (const key of sortedKeys) {
|
|
242
|
+
const patternClasses = key.split(" ");
|
|
243
|
+
const mergedClass = mergeMap.get(key)!;
|
|
244
|
+
|
|
245
|
+
// Check if all pattern classes are present
|
|
246
|
+
let allPresent = true;
|
|
247
|
+
for (const cls of patternClasses) {
|
|
248
|
+
if (!result.includes(cls)) {
|
|
249
|
+
allPresent = false;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (allPresent) {
|
|
255
|
+
// Remove pattern classes and add merged class
|
|
256
|
+
result = result.filter((c) => !patternClasses.includes(c));
|
|
257
|
+
merged.push(mergedClass);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Return: merged classes + remaining prefixed + other classes
|
|
262
|
+
return [...merged, ...result, ...otherClasses];
|
|
263
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Co-occurrence Optimizer - Extractors
|
|
3
|
+
*
|
|
4
|
+
* Pluggable class extraction strategies for different source formats.
|
|
5
|
+
* Each extractor takes source content and returns ClassUsage arrays.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ClassUsage } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extractor interface - implement this for custom source formats
|
|
12
|
+
*/
|
|
13
|
+
export interface ClassExtractor {
|
|
14
|
+
/** Extractor name for debugging */
|
|
15
|
+
name: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract class usages from source content
|
|
19
|
+
* @param content - Source content (HTML, JSX, etc.)
|
|
20
|
+
* @param options - Extractor-specific options
|
|
21
|
+
*/
|
|
22
|
+
extract(content: string, options?: ExtractorOptions): ClassUsage[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Common extractor options
|
|
27
|
+
*/
|
|
28
|
+
export interface ExtractorOptions {
|
|
29
|
+
/** Only extract classes starting with this prefix (default: "_") */
|
|
30
|
+
classPrefix?: string;
|
|
31
|
+
/** Minimum classes per element to include (default: 2) */
|
|
32
|
+
minClasses?: number;
|
|
33
|
+
/** Source identifier for debugging */
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* HTML Class Extractor
|
|
39
|
+
*
|
|
40
|
+
* Extracts class usages from HTML content by parsing class="" attributes.
|
|
41
|
+
* Works with static HTML, SSR output, template strings, etc.
|
|
42
|
+
*/
|
|
43
|
+
export class HtmlExtractor implements ClassExtractor {
|
|
44
|
+
name = "html";
|
|
45
|
+
|
|
46
|
+
private pattern = /class\s*=\s*"([^"]+)"/g;
|
|
47
|
+
|
|
48
|
+
extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
|
|
49
|
+
const { classPrefix = "_", minClasses = 2, source = "html" } = options;
|
|
50
|
+
|
|
51
|
+
const usages: ClassUsage[] = [];
|
|
52
|
+
let match: RegExpExecArray | null;
|
|
53
|
+
|
|
54
|
+
this.pattern.lastIndex = 0;
|
|
55
|
+
while ((match = this.pattern.exec(content)) !== null) {
|
|
56
|
+
const classValue = match[1].trim();
|
|
57
|
+
if (!classValue) continue;
|
|
58
|
+
|
|
59
|
+
// Split by whitespace and filter by prefix
|
|
60
|
+
const classes = classValue
|
|
61
|
+
.split(/\s+/)
|
|
62
|
+
.filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
63
|
+
|
|
64
|
+
// Only include elements with minClasses or more matching classes
|
|
65
|
+
if (classes.length >= minClasses) {
|
|
66
|
+
classes.sort();
|
|
67
|
+
usages.push({ classes, source: `${source}:${match.index}` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return usages;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* JSX Class Extractor (simplified)
|
|
77
|
+
*
|
|
78
|
+
* Extracts class usages from JSX/TSX by finding className attributes.
|
|
79
|
+
* Note: This is a simplified regex-based extractor. For full AST support,
|
|
80
|
+
* consider using @babel/parser or TypeScript compiler.
|
|
81
|
+
*/
|
|
82
|
+
export class JsxExtractor implements ClassExtractor {
|
|
83
|
+
name = "jsx";
|
|
84
|
+
|
|
85
|
+
// Match className="..." and className={'...'}
|
|
86
|
+
private stringPattern = /className\s*=\s*"([^"]+)"/g;
|
|
87
|
+
private templatePattern = /className\s*=\s*\{['"`]([^'"`]+)['"`]\}/g;
|
|
88
|
+
|
|
89
|
+
extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
|
|
90
|
+
const { classPrefix = "_", minClasses = 2, source = "jsx" } = options;
|
|
91
|
+
|
|
92
|
+
const usages: ClassUsage[] = [];
|
|
93
|
+
|
|
94
|
+
// Extract from string literals
|
|
95
|
+
this.extractFromPattern(content, this.stringPattern, classPrefix, minClasses, source, usages);
|
|
96
|
+
// Extract from template literals
|
|
97
|
+
this.extractFromPattern(content, this.templatePattern, classPrefix, minClasses, source, usages);
|
|
98
|
+
|
|
99
|
+
return usages;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private extractFromPattern(
|
|
103
|
+
content: string,
|
|
104
|
+
pattern: RegExp,
|
|
105
|
+
classPrefix: string,
|
|
106
|
+
minClasses: number,
|
|
107
|
+
source: string,
|
|
108
|
+
usages: ClassUsage[]
|
|
109
|
+
): void {
|
|
110
|
+
let match: RegExpExecArray | null;
|
|
111
|
+
|
|
112
|
+
pattern.lastIndex = 0;
|
|
113
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
114
|
+
const classValue = match[1].trim();
|
|
115
|
+
if (!classValue) continue;
|
|
116
|
+
|
|
117
|
+
const classes = classValue
|
|
118
|
+
.split(/\s+/)
|
|
119
|
+
.filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
120
|
+
|
|
121
|
+
if (classes.length >= minClasses) {
|
|
122
|
+
classes.sort();
|
|
123
|
+
usages.push({ classes, source: `${source}:${match.index}` });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Svelte Class Extractor (simplified)
|
|
131
|
+
*
|
|
132
|
+
* Extracts class usages from Svelte templates.
|
|
133
|
+
* Handles both class="..." and class:directive syntax.
|
|
134
|
+
*/
|
|
135
|
+
export class SvelteExtractor implements ClassExtractor {
|
|
136
|
+
name = "svelte";
|
|
137
|
+
|
|
138
|
+
private classPattern = /class\s*=\s*"([^"]+)"/g;
|
|
139
|
+
|
|
140
|
+
extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
|
|
141
|
+
const { classPrefix = "_", minClasses = 2, source = "svelte" } = options;
|
|
142
|
+
|
|
143
|
+
const usages: ClassUsage[] = [];
|
|
144
|
+
let match: RegExpExecArray | null;
|
|
145
|
+
|
|
146
|
+
this.classPattern.lastIndex = 0;
|
|
147
|
+
while ((match = this.classPattern.exec(content)) !== null) {
|
|
148
|
+
const classValue = match[1].trim();
|
|
149
|
+
if (!classValue) continue;
|
|
150
|
+
|
|
151
|
+
// Handle Svelte's {expression} syntax by removing them
|
|
152
|
+
const cleanValue = classValue.replace(/\{[^}]+\}/g, " ");
|
|
153
|
+
|
|
154
|
+
const classes = cleanValue
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
157
|
+
|
|
158
|
+
if (classes.length >= minClasses) {
|
|
159
|
+
classes.sort();
|
|
160
|
+
usages.push({ classes, source: `${source}:${match.index}` });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return usages;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Multi-source extractor
|
|
170
|
+
*
|
|
171
|
+
* Combines usages from multiple sources/extractors.
|
|
172
|
+
* Useful for analyzing entire projects with mixed file types.
|
|
173
|
+
*/
|
|
174
|
+
export class MultiExtractor implements ClassExtractor {
|
|
175
|
+
name = "multi";
|
|
176
|
+
|
|
177
|
+
private extractors: Map<string, ClassExtractor>;
|
|
178
|
+
|
|
179
|
+
constructor() {
|
|
180
|
+
this.extractors = new Map<string, ClassExtractor>([
|
|
181
|
+
["html", new HtmlExtractor()],
|
|
182
|
+
["jsx", new JsxExtractor()],
|
|
183
|
+
["tsx", new JsxExtractor()],
|
|
184
|
+
["svelte", new SvelteExtractor()],
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Register a custom extractor for a file extension
|
|
190
|
+
*/
|
|
191
|
+
register(extension: string, extractor: ClassExtractor): void {
|
|
192
|
+
this.extractors.set(extension, extractor);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract from content using the appropriate extractor
|
|
197
|
+
*/
|
|
198
|
+
extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
|
|
199
|
+
// Default to HTML extractor
|
|
200
|
+
const htmlExtractor = this.extractors.get("html")!;
|
|
201
|
+
return htmlExtractor.extract(content, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Extract from content with explicit file type
|
|
206
|
+
*/
|
|
207
|
+
extractWithType(
|
|
208
|
+
content: string,
|
|
209
|
+
fileType: string,
|
|
210
|
+
options: ExtractorOptions = {}
|
|
211
|
+
): ClassUsage[] {
|
|
212
|
+
const extractor = this.extractors.get(fileType) || this.extractors.get("html")!;
|
|
213
|
+
return extractor.extract(content, options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract from multiple files
|
|
218
|
+
*/
|
|
219
|
+
extractFromFiles(
|
|
220
|
+
files: Array<{ content: string; path: string }>,
|
|
221
|
+
options: ExtractorOptions = {}
|
|
222
|
+
): ClassUsage[] {
|
|
223
|
+
const allUsages: ClassUsage[] = [];
|
|
224
|
+
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
const ext = file.path.split(".").pop() || "html";
|
|
227
|
+
const extractor = this.extractors.get(ext) || this.extractors.get("html")!;
|
|
228
|
+
const usages = extractor.extract(file.content, {
|
|
229
|
+
...options,
|
|
230
|
+
source: file.path,
|
|
231
|
+
});
|
|
232
|
+
allUsages.push(...usages);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return allUsages;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Default extractors
|
|
240
|
+
export const htmlExtractor = new HtmlExtractor();
|
|
241
|
+
export const jsxExtractor = new JsxExtractor();
|
|
242
|
+
export const svelteExtractor = new SvelteExtractor();
|
|
243
|
+
export const multiExtractor = new MultiExtractor();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash functions for CSS class name generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DJB2 hash function
|
|
7
|
+
* A simple but effective string hashing algorithm
|
|
8
|
+
*/
|
|
9
|
+
export function djb2Hash(s: string): number {
|
|
10
|
+
let hash = 5381;
|
|
11
|
+
for (let i = 0; i < s.length; i++) {
|
|
12
|
+
const c = s.charCodeAt(i);
|
|
13
|
+
// hash * 33 + c (using unsigned 32-bit arithmetic)
|
|
14
|
+
hash = ((hash << 5) + hash + c) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return hash;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a number to base36 string (0-9a-z)
|
|
21
|
+
* Takes lower 24 bits to keep class names short (4-5 chars)
|
|
22
|
+
*/
|
|
23
|
+
export function toBase36(n: number): string {
|
|
24
|
+
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
25
|
+
// Take lower 24 bits
|
|
26
|
+
n = n & 0xffffff;
|
|
27
|
+
if (n === 0) return "0";
|
|
28
|
+
let result = "";
|
|
29
|
+
while (n > 0) {
|
|
30
|
+
result = chars[n % 36] + result;
|
|
31
|
+
n = Math.floor(n / 36);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a class name from a CSS declaration string
|
|
38
|
+
* @param decl - CSS declaration like "display:flex"
|
|
39
|
+
* @param prefix - Class name prefix (default: "_")
|
|
40
|
+
*/
|
|
41
|
+
export function hashClassName(decl: string, prefix = "_"): string {
|
|
42
|
+
const hash = djb2Hash(decl);
|
|
43
|
+
return prefix + toBase36(hash);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a merged class name from combined declarations
|
|
48
|
+
* @param declarations - Array of CSS declarations
|
|
49
|
+
* @param prefix - Class name prefix (default: "_m" for merged)
|
|
50
|
+
*/
|
|
51
|
+
export function hashMergedClassName(declarations: string[], prefix = "_m"): string {
|
|
52
|
+
const combined = declarations.sort().join(";");
|
|
53
|
+
return prefix + toBase36(djb2Hash(combined));
|
|
54
|
+
}
|