ruscus 0.0.0-stage → 0.0.2

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/index.mjs ADDED
@@ -0,0 +1,661 @@
1
+ //#region src/types/ids.ts
2
+ const ELEMENT_ROOT = "root";
3
+ //#endregion
4
+ //#region src/projection/elements.ts
5
+ /**
6
+ * Project a root node and its children
7
+ */
8
+ function projectTree(namespace, elements, { format, hasVariants, name }) {
9
+ return {
10
+ ...projectElements(namespace, elements, {
11
+ format,
12
+ name
13
+ }),
14
+ [ELEMENT_ROOT]: projectNode(name.forClass(namespace), {
15
+ format,
16
+ hasVariants,
17
+ name
18
+ })
19
+ };
20
+ }
21
+ /**
22
+ * Project elements under a namespace
23
+ */
24
+ function projectElements(namespace, elements, { format, name }) {
25
+ return Object.entries(elements).reduce((previous, [key, element]) => {
26
+ if (element) previous[key] = projectElement([...namespace, key], element, {
27
+ format,
28
+ name
29
+ });
30
+ return previous;
31
+ }, {});
32
+ }
33
+ /**
34
+ * Project an element as a class name, selector, or child tree
35
+ */
36
+ function projectElement(path, element, options) {
37
+ const { children } = element;
38
+ const hasVariants = element.variants !== void 0;
39
+ return Object.keys(children).length ? projectTree(path, children, {
40
+ ...options,
41
+ hasVariants
42
+ }) : projectNode(options.name.forClass(path), {
43
+ ...options,
44
+ hasVariants
45
+ });
46
+ }
47
+ /**
48
+ * Project a node as a class name or variant projector
49
+ */
50
+ function projectNode(className, { format, hasVariants, name }) {
51
+ const root = formatClass(className, format);
52
+ return hasVariants ? createVariantProjector(root, format, name) : root;
53
+ }
54
+ /**
55
+ * Format a class name
56
+ */
57
+ function formatClass(name, format) {
58
+ return format === "selectors" ? `.${name}` : name;
59
+ }
60
+ /**
61
+ * Create a variant projector anchored to a compile-time root
62
+ */
63
+ function createVariantProjector(anchor, format, name) {
64
+ const joiner = format === "selectors" ? "" : " ";
65
+ return (...vs) => {
66
+ return [anchor, ...vs.filter(Boolean).map((v) => formatClass(name.forVariant(v), format))].join(joiner);
67
+ };
68
+ }
69
+ //#endregion
70
+ //#region src/styles.ts
71
+ /**
72
+ * Convert a camel-cased value to kebab case
73
+ */
74
+ function camelToKebab(camel) {
75
+ return camel.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
76
+ }
77
+ /**
78
+ * Merge multiple styles on top of a base
79
+ */
80
+ function mergeStyles(base, ...others) {
81
+ const merged = { ...base };
82
+ for (const other of others) Object.assign(merged, other);
83
+ return merged;
84
+ }
85
+ var PropertyProjector = class {
86
+ names;
87
+ /**
88
+ * Create a mapper of camel-cased values to CSS properties
89
+ */
90
+ constructor() {
91
+ this.names = /* @__PURE__ */ new Map();
92
+ }
93
+ /**
94
+ * Project a camel-cased value as a CSS property
95
+ */
96
+ project(camel) {
97
+ let prop = this.names.get(camel);
98
+ if (prop === void 0) {
99
+ prop = camelToKebab(camel);
100
+ this.names.set(camel, prop);
101
+ }
102
+ return prop;
103
+ }
104
+ };
105
+ //#endregion
106
+ //#region src/projection/identifiers.ts
107
+ /**
108
+ * Project a component's identifiers as a map of labels to values
109
+ */
110
+ function projectIdentifiers(component) {
111
+ return Object.fromEntries(Object.entries(component.identifiers).map(([key, type]) => {
112
+ const scoped = `${component.id}--${camelToKebab(key)}`;
113
+ return [key, type === "variable" ? `--${scoped}` : scoped];
114
+ }));
115
+ }
116
+ //#endregion
117
+ //#region src/contexts.ts
118
+ /**
119
+ * Build a context for derived component styles
120
+ */
121
+ function buildComponentContext(component, { name }) {
122
+ return {
123
+ els: projectTree([component.id], component.elements, {
124
+ format: "selectors",
125
+ hasVariants: component.variants !== void 0,
126
+ name
127
+ }),
128
+ ids: projectIdentifiers(component),
129
+ variants: createVariantProjector("&", "selectors", name)
130
+ };
131
+ }
132
+ /**
133
+ * Build a context for derived element styles
134
+ */
135
+ function buildElementContext(namespace, element, { name }) {
136
+ return {
137
+ els: projectTree(namespace, element.children, {
138
+ format: "selectors",
139
+ hasVariants: element.variants !== void 0,
140
+ name
141
+ }),
142
+ variants: createVariantProjector("&", "selectors", name)
143
+ };
144
+ }
145
+ //#endregion
146
+ //#region src/fragments.ts
147
+ /**
148
+ * Create a fragment factory
149
+ */
150
+ function createFactory(scope) {
151
+ const factory = ((styles = {}) => ({
152
+ nested: (definitions) => {
153
+ const nested = factory(styles);
154
+ for (const fragment of Object.values(definitions)) fragment.scope = "relative";
155
+ nested.styles.nested = { ...definitions };
156
+ return nested;
157
+ },
158
+ scope,
159
+ styles: {
160
+ nested: {},
161
+ root: styles
162
+ }
163
+ }));
164
+ factory.rules = (definitions) => factory().nested(definitions);
165
+ return factory;
166
+ }
167
+ const defineAbsoluteFragment = createFactory("absolute");
168
+ const defineRelativeFragment = createFactory("relative");
169
+ //#endregion
170
+ //#region src/naming/shared.ts
171
+ var Naming = class {
172
+ component;
173
+ /**
174
+ * Create a naming strategy for a component
175
+ */
176
+ constructor(component) {
177
+ this.component = component;
178
+ }
179
+ /**
180
+ * Produce the class name for a variant
181
+ */
182
+ forVariant(name) {
183
+ return `is-${name}`;
184
+ }
185
+ };
186
+ //#endregion
187
+ //#region src/naming/compact.ts
188
+ const SEPARATOR = "\0";
189
+ var CompactNaming = class extends Naming {
190
+ prefix;
191
+ suffixes;
192
+ /**
193
+ * Build compact suffixes for all element paths
194
+ */
195
+ constructor(component) {
196
+ super(component);
197
+ this.prefix = `r${hash(component.id)}`;
198
+ this.suffixes = this.buildSuffixes(component.elements);
199
+ }
200
+ /**
201
+ * Produce a compact class name
202
+ */
203
+ forClass(levels) {
204
+ if (levels.length <= 1) return this.prefix;
205
+ const suffix = this.suffixes.get(levels.slice(1).join(SEPARATOR));
206
+ if (suffix === void 0) throw new Error(`Unknown element path for compact naming: ${levels.join(" > ")}`);
207
+ return `${this.prefix}_${suffix}`;
208
+ }
209
+ /**
210
+ * Map each element path to a compact suffix
211
+ */
212
+ buildSuffixes(elements) {
213
+ const paths = this.collectElementPaths(elements).sort((alfa, bravo) => alfa.localeCompare(bravo));
214
+ const suffixes = /* @__PURE__ */ new Map();
215
+ for (const [index, path] of paths.entries()) suffixes.set(path, index.toString(36));
216
+ return suffixes;
217
+ }
218
+ /**
219
+ * Collect element paths in depth-first order
220
+ */
221
+ collectElementPaths(elements, base = "") {
222
+ const paths = [];
223
+ for (const name of Object.keys(elements).sort()) {
224
+ const element = elements[name];
225
+ if (element === void 0) continue;
226
+ const path = base.length === 0 ? name : `${base}${SEPARATOR}${name}`;
227
+ paths.push(path);
228
+ paths.push(...this.collectElementPaths(element.children, path));
229
+ }
230
+ return paths;
231
+ }
232
+ };
233
+ /**
234
+ * Build a short hash for a value using a 32-bit FNV-1a algorithm
235
+ */
236
+ function hash(value, length = 6) {
237
+ let hash = 2166136261;
238
+ for (const character of value) {
239
+ hash ^= character.charCodeAt(0);
240
+ hash = Math.imul(hash, 16777619);
241
+ }
242
+ return (hash >>> 0).toString(36).slice(0, length).padStart(length, "0");
243
+ }
244
+ //#endregion
245
+ //#region src/naming/verbose.ts
246
+ var VerboseNaming = class extends Naming {
247
+ /**
248
+ * Produce a BEM-style class name
249
+ */
250
+ forClass(levels) {
251
+ return levels.join("__");
252
+ }
253
+ };
254
+ //#endregion
255
+ //#region src/naming.ts
256
+ /**
257
+ * Create a name generator for a component
258
+ */
259
+ function useNaming(component, scheme = "verbose") {
260
+ return scheme === "compact" ? new CompactNaming(component) : new VerboseNaming(component);
261
+ }
262
+ //#endregion
263
+ //#region src/compilation.ts
264
+ /**
265
+ * Generate CSS for a component's styles
266
+ */
267
+ function styleComponent(component, config = {}) {
268
+ return new ComponentCompiler(component, config).compile();
269
+ }
270
+ var ComponentCompiler = class {
271
+ blocks;
272
+ component;
273
+ config;
274
+ indent;
275
+ name;
276
+ /**
277
+ * Create a compiler for a component's styles
278
+ */
279
+ constructor(component, config) {
280
+ this.config = {
281
+ indentation: 2,
282
+ naming: "verbose",
283
+ properties: new PropertyProjector(),
284
+ ...config
285
+ };
286
+ this.blocks = [];
287
+ this.component = component;
288
+ this.indent = " ".repeat(this.config.indentation);
289
+ this.name = useNaming(component, config.naming);
290
+ }
291
+ /**
292
+ * Produce CSS for the component's styles
293
+ */
294
+ compile() {
295
+ const { component, name } = this;
296
+ const { elements, variants } = component;
297
+ const root = this.buildSelector();
298
+ const context = buildComponentContext(component, { name });
299
+ this.blocks.push(this.formatRule(root, component.styles));
300
+ if (variants) this.addVariantRules(root, variants(defineRelativeFragment, context));
301
+ for (const [name, element] of Object.entries(elements)) this.addElementStyles([name], element);
302
+ for (const provider of component.nested) this.addRelativeFragmentRules(root, provider(defineRelativeFragment, context));
303
+ for (const provider of component.global) this.addGlobalFragmentRules(provider(defineAbsoluteFragment, context));
304
+ return this.blocks.filter(Boolean).join("\n\n");
305
+ }
306
+ /**
307
+ * Add base styles for a component's elements
308
+ */
309
+ addElementStyles(path, element) {
310
+ const { children, variants } = element;
311
+ const selector = this.buildSelector(...path);
312
+ const context = buildElementContext([this.component.id, ...path], element, { name: this.name });
313
+ this.blocks.push(this.formatRule(selector, element.styles));
314
+ if (variants) this.addVariantRules(selector, variants(defineRelativeFragment, context));
315
+ for (const provider of element.nested) this.addRelativeFragmentRules(selector, provider(defineRelativeFragment, context));
316
+ for (const [name, child] of Object.entries(children)) this.addElementStyles([...path, name], child);
317
+ }
318
+ /**
319
+ * Add variant rules for component or element variants
320
+ */
321
+ addVariantRules(anchor, definitions) {
322
+ for (const [name, fragment] of Object.entries(definitions)) this.blocks.push(this.formatFragmentRules(`${anchor}.${this.name.forVariant(name)}`, fragment));
323
+ }
324
+ /**
325
+ * Add global fragment rules
326
+ */
327
+ addGlobalFragmentRules(definitions) {
328
+ for (const [key, fragment] of Object.entries(definitions)) this.blocks.push(this.formatFragmentRules(key, fragment));
329
+ }
330
+ /**
331
+ * Add relative fragment rules
332
+ */
333
+ addRelativeFragmentRules(anchor, definitions) {
334
+ for (const [key, fragment] of Object.entries(definitions)) this.blocks.push(this.formatFragmentRules(this.buildNestedSelector(anchor, key), fragment));
335
+ }
336
+ /**
337
+ * Format a fragment as CSS at a selector
338
+ */
339
+ formatFragmentRules(parentSelector, fragment) {
340
+ const { nested, root: styles } = fragment.styles;
341
+ const nestedEntries = Object.entries(nested);
342
+ if (nestedEntries.length === 0) return this.formatRule(parentSelector, styles);
343
+ const inner = [];
344
+ const declarations = this.formatDeclarations(styles);
345
+ if (declarations) inner.push(declarations);
346
+ for (const [key, child] of nestedEntries) {
347
+ const rule = this.formatFragmentRules(key.startsWith("&") ? `${parentSelector}${key.slice(1)}` : key, child);
348
+ if (rule) inner.push(rule);
349
+ }
350
+ if (!inner.length) return null;
351
+ return this.formatNestedBlock(parentSelector, inner);
352
+ }
353
+ /**
354
+ * Format declaration as key/value pairs
355
+ */
356
+ formatDeclarations(styles) {
357
+ const { config: { properties }, indent } = this;
358
+ const props = Object.entries(styles).filter(([_, v]) => v !== void 0);
359
+ if (props.length === 0) return null;
360
+ return props.map(([p]) => `${indent}${properties.project(p)}: ${styles[p]};`).join("\n");
361
+ }
362
+ /**
363
+ * Wrap nested rules in a selector block
364
+ */
365
+ formatNestedBlock(selector, inner) {
366
+ const { indent } = this;
367
+ return `${selector} {\n${inner.map((rule) => rule.split("\n").map((line) => `${indent}${line}`).join("\n")).join("\n\n")}\n}`;
368
+ }
369
+ /**
370
+ * Build a selector for a nested rule key
371
+ */
372
+ buildNestedSelector(anchor, key) {
373
+ if (key.startsWith("&")) return `${anchor}${key.slice(1)}`;
374
+ else if (key === anchor) return anchor;
375
+ else return `${anchor} ${key}`;
376
+ }
377
+ /**
378
+ * Build a selector for a component element
379
+ */
380
+ buildSelector(...parts) {
381
+ return `.${this.name.forClass([this.component.id, ...parts])}`;
382
+ }
383
+ /**
384
+ * Format a CSS rule
385
+ */
386
+ formatRule(selector, styles) {
387
+ const body = this.formatDeclarations(styles);
388
+ return body === null ? null : `${selector} {\n${body}\n}`;
389
+ }
390
+ };
391
+ //#endregion
392
+ //#region src/builder.ts
393
+ var Builder = class {
394
+ current;
395
+ /**
396
+ * Create a builder that progressively updates its state
397
+ */
398
+ constructor(defaultState) {
399
+ this.current = defaultState;
400
+ }
401
+ /**
402
+ * Set a state field
403
+ *
404
+ * @throws if the field already has a value
405
+ */
406
+ setStateField(key, value) {
407
+ const current = this.current[key];
408
+ if (current !== void 0) {
409
+ if (typeof current === "object" ? Object.keys(current).length > 0 : true) throw new Error(`${key} may only be set once`);
410
+ }
411
+ this.current[key] = value;
412
+ }
413
+ };
414
+ //#endregion
415
+ //#region src/traits.ts
416
+ /**
417
+ * Merge traits into a collection of styles
418
+ */
419
+ function mergeTraits(...traits) {
420
+ return traits.reduce((previous, { styles }) => {
421
+ previous.root = mergeStyles(previous.root, styles.root);
422
+ Object.assign(previous.nested, styles.nested);
423
+ return previous;
424
+ }, {
425
+ nested: {},
426
+ root: {}
427
+ });
428
+ }
429
+ /**
430
+ * Define a trait
431
+ */
432
+ function defineTrait(build) {
433
+ return build(defineRelativeFragment);
434
+ }
435
+ //#endregion
436
+ //#region src/utils.ts
437
+ /**
438
+ * Cast a value
439
+ */
440
+ function cast(value) {
441
+ return value;
442
+ }
443
+ /**
444
+ * Resolve an eager value or a thunk
445
+ */
446
+ function resolve(value) {
447
+ return typeof value === "function" ? value() : value;
448
+ }
449
+ //#endregion
450
+ //#region src/elements.ts
451
+ /**
452
+ * Define an element
453
+ */
454
+ function defineElement(styles = {}) {
455
+ return new Element().css(styles);
456
+ }
457
+ /**
458
+ * Resolve element providers to concrete elements
459
+ */
460
+ function resolveElements(providers) {
461
+ return Object.entries(providers).reduce((els, [name, provider]) => {
462
+ els[name] = resolve(provider).current;
463
+ return els;
464
+ }, {});
465
+ }
466
+ var Element = class extends Builder {
467
+ /**
468
+ * Create an element
469
+ */
470
+ constructor() {
471
+ super({
472
+ children: {},
473
+ nested: [],
474
+ styles: {}
475
+ });
476
+ }
477
+ /**
478
+ * Define child elements
479
+ */
480
+ children(definitions) {
481
+ this.setStateField("children", resolveElements(resolve(definitions)));
482
+ return this;
483
+ }
484
+ /**
485
+ * Apply styles to the element
486
+ */
487
+ css(styles) {
488
+ const { current } = this;
489
+ current.styles = mergeStyles(current.styles, styles);
490
+ return this;
491
+ }
492
+ /**
493
+ * Apply traits to the element
494
+ */
495
+ is(...traits) {
496
+ const { nested, root } = mergeTraits(...traits);
497
+ this.current.styles = mergeStyles(this.current.styles, root);
498
+ if (Object.keys(nested).length > 0) this.current.nested.push(() => nested);
499
+ return this;
500
+ }
501
+ /**
502
+ * Define nested styles for the element
503
+ */
504
+ nested(provider) {
505
+ this.current.nested.push(provider);
506
+ return this;
507
+ }
508
+ /**
509
+ * Define variants for the element
510
+ */
511
+ variants(provider) {
512
+ this.setStateField("variants", provider);
513
+ return this;
514
+ }
515
+ };
516
+ //#endregion
517
+ //#region src/internal.ts
518
+ const build = Symbol.for("ruscus/build");
519
+ const identifiers = Symbol.for("ruscus/identifiers");
520
+ /**
521
+ * Apply internal annotations to a component
522
+ */
523
+ function annotateComponent(component, annotations) {
524
+ const annotate = (key, value) => {
525
+ Object.defineProperty(component, key, {
526
+ configurable: false,
527
+ enumerable: false,
528
+ value,
529
+ writable: false
530
+ });
531
+ };
532
+ annotate(build, annotations.buildPlan);
533
+ annotate(identifiers, annotations.identifiers);
534
+ return component;
535
+ }
536
+ /**
537
+ * Extract identifiers from a component
538
+ */
539
+ function extractIdentifiers(component) {
540
+ return component[identifiers];
541
+ }
542
+ //#endregion
543
+ //#region src/projection.ts
544
+ /**
545
+ * Project a component as class names
546
+ */
547
+ function projectComponent({ current }, name) {
548
+ return projectTree([current.id], current.elements, {
549
+ format: "names",
550
+ hasVariants: current.variants !== void 0,
551
+ name
552
+ });
553
+ }
554
+ //#endregion
555
+ //#region src/components.ts
556
+ var Component = class extends Builder {
557
+ id;
558
+ /**
559
+ * Create a component
560
+ */
561
+ constructor(name, { namespace = [] } = {}) {
562
+ const id = [...namespace, name].filter(Boolean).join("--");
563
+ super({
564
+ elements: {},
565
+ global: [],
566
+ id,
567
+ identifiers: {},
568
+ nested: [],
569
+ styles: {}
570
+ });
571
+ this.id = id;
572
+ }
573
+ /**
574
+ * Project the component into class names and identifiers
575
+ */
576
+ apply() {
577
+ const name = useNaming(this.current);
578
+ return annotateComponent(projectComponent(this, name), {
579
+ buildPlan: {
580
+ compile: (config = {}) => styleComponent(this.current, config),
581
+ component: this.current
582
+ },
583
+ identifiers: cast(projectIdentifiers(this.current))
584
+ });
585
+ }
586
+ /**
587
+ * Apply styles to the component root
588
+ */
589
+ css(styles) {
590
+ const { current } = this;
591
+ current.styles = mergeStyles(current.styles, styles);
592
+ return this;
593
+ }
594
+ /**
595
+ * Define component-specific identifiers
596
+ */
597
+ identifiers(types) {
598
+ this.setStateField("identifiers", types);
599
+ return this;
600
+ }
601
+ /**
602
+ * Define the component's elements
603
+ */
604
+ elements(provider) {
605
+ this.setStateField("elements", resolveElements(provider(defineElement, { ids: projectIdentifiers(this.current) })));
606
+ return this;
607
+ }
608
+ /**
609
+ * Apply reusable trait styles and nested rules
610
+ */
611
+ is(...traits) {
612
+ const { nested, root } = mergeTraits(...traits);
613
+ this.current.styles = mergeStyles(this.current.styles, root);
614
+ if (Object.keys(nested).length > 0) this.current.nested.push(() => nested);
615
+ return this;
616
+ }
617
+ /**
618
+ * Define global styles for the component
619
+ */
620
+ global(definitions) {
621
+ this.current.global.push(definitions);
622
+ return this;
623
+ }
624
+ /**
625
+ * Define nested styles for the component
626
+ */
627
+ nested(definitions) {
628
+ this.current.nested.push(definitions);
629
+ return this;
630
+ }
631
+ /**
632
+ * Define variants for the component root
633
+ */
634
+ variants(provider) {
635
+ this.setStateField("variants", provider);
636
+ return this;
637
+ }
638
+ };
639
+ /**
640
+ * Define a component
641
+ */
642
+ function defineComponent(name) {
643
+ return new Component(name);
644
+ }
645
+ //#endregion
646
+ //#region src/queries.ts
647
+ /**
648
+ * Define a provider of named queries
649
+ */
650
+ function defineQueries(queries) {
651
+ const build = (blocks) => {
652
+ return Object.entries(blocks).reduce((previous, [label, rules]) => {
653
+ const query = queries[label];
654
+ if (rules && query) previous[query] = defineAbsoluteFragment().nested(rules);
655
+ return previous;
656
+ }, {});
657
+ };
658
+ return Object.assign(build, queries);
659
+ }
660
+ //#endregion
661
+ export { defineComponent as component, extractIdentifiers as identifiers, defineQueries as queries, styleComponent as style, defineTrait as trait };
@@ -0,0 +1,20 @@
1
+ import { Plugin } from "vite";
2
+ //#region src/naming.d.ts
3
+ /**
4
+ * An available naming scheme for classes
5
+ */
6
+ type NamingScheme = 'compact' | 'verbose';
7
+ //#endregion
8
+ //#region src/vite/plugin.d.ts
9
+ /**
10
+ * Plugin options
11
+ */
12
+ type PluginOptions = {
13
+ naming?: NamingScheme;
14
+ };
15
+ /**
16
+ * Define the Vite plugin
17
+ */
18
+ declare function plugin(options?: PluginOptions): Plugin;
19
+ //#endregion
20
+ export { plugin as default };
package/dist/vite.mjs ADDED
Binary file