diff --git a/apps/lapikit.dev/src/routes/+page.svelte b/apps/lapikit.dev/src/routes/+page.svelte
index 854486e..3f117ed 100644
--- a/apps/lapikit.dev/src/routes/+page.svelte
+++ b/apps/lapikit.dev/src/routes/+page.svelte
@@ -1,5 +1,5 @@
Welcome to SvelteKit
@@ -15,4 +15,10 @@
demo
Labs merge Class and Styles Core
-Demo
+Demo
diff --git a/packages/lapikit/CHANGELOG.md b/packages/lapikit/CHANGELOG.md
index 29dd946..6c76a7b 100644
--- a/packages/lapikit/CHANGELOG.md
+++ b/packages/lapikit/CHANGELOG.md
@@ -19,6 +19,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.3.8] - 2026-02-01
+
+### Updated
+
+- Optmize preprocess lili with new function
+- Rewrite all mapping code
+- Add test for critical function in lili core
+
## [0.3.7] - 2026-01-25
### Added
diff --git a/packages/lapikit/package.json b/packages/lapikit/package.json
index ffabe1d..67ea46c 100644
--- a/packages/lapikit/package.json
+++ b/packages/lapikit/package.json
@@ -1,6 +1,6 @@
{
"name": "lapikit",
- "version": "0.3.7",
+ "version": "0.3.8",
"license": "MIT",
"publishConfig": {
"access": "public"
diff --git a/packages/lapikit/src/lib/entry-bundler.ts b/packages/lapikit/src/lib/entry-bundler.ts
index 8341dd4..f99b0f9 100644
--- a/packages/lapikit/src/lib/entry-bundler.ts
+++ b/packages/lapikit/src/lib/entry-bundler.ts
@@ -1,135 +1,8 @@
-const components: readonly string[] = ['btn'] as const;
-
-function componentName(shortName: string): string {
- return 'Kit' + shortName.charAt(0).toUpperCase() + shortName.slice(1);
-}
-
-// plugins lapikit
-const lapikitPlugins = {
- repl: {
- components: ['repl'],
- ref: '@lapikit/repl'
- }
-} as const;
-
-type LapikitPreprocessOptions = {
- plugins?: string[];
-};
+import { liliCore } from '$lib/labs/compiler/preprocess/index.js';
+import type { LapikitPreprocessOptions } from '$lib/labs/compiler/types/options.js';
function lapikitPreprocess(options?: LapikitPreprocessOptions) {
- return {
- markup({ content }: { content: string; filename?: string }) {
- const allComponents = [...components];
- const componentToRef = new Map();
-
- components.forEach((comp) => {
- componentToRef.set(comp, 'lapikit/labs/components');
- });
-
- // plugins
- if (options?.plugins) {
- options.plugins.forEach((pluginKey) => {
- const plugin = lapikitPlugins[pluginKey as keyof typeof lapikitPlugins];
- if (plugin) {
- plugin.components.forEach((comp) => {
- if (!allComponents.includes(comp)) {
- allComponents.push(comp);
- }
- componentToRef.set(comp, plugin.ref);
- });
- }
- });
- }
-
- const hasComponent = allComponents.some((comp) => content.includes(`();
-
- let hasChanges = true;
- while (hasChanges) {
- hasChanges = false;
-
- for (const shortName of allComponents) {
- const componentNameStr = componentName(shortName);
-
- const attrPattern = `(?:[^>"']|"[^"]*"|'[^']*')*?`;
-
- const selfClosingRegex = new RegExp(``, 'g');
-
- const pairRegex = new RegExp(
- `([\\s\\S]*?)<\\/kit:${shortName}\\s*>`,
- 'g'
- );
-
- let newContent = processedContent.replace(selfClosingRegex, (fullMatch, attrs) => {
- hasChanges = true;
- const ref = componentToRef.get(shortName) || 'lapikit/labs/components';
- importedComponents.set(componentNameStr, ref);
- return `<${componentNameStr}${attrs} />`;
- });
-
- newContent = newContent.replace(pairRegex, (fullMatch, attrs, children) => {
- hasChanges = true;
- const ref = componentToRef.get(shortName) || 'lapikit/labs/components';
- importedComponents.set(componentNameStr, ref);
- return `<${componentNameStr}${attrs}>${children}${componentNameStr}>`;
- });
-
- if (newContent !== processedContent) {
- processedContent = newContent;
- }
- }
- }
-
- if (processedContent === content) return;
-
- if (importedComponents.size > 0) {
- const importsByRef = new Map();
- importedComponents.forEach((ref, component) => {
- if (!importsByRef.has(ref)) {
- importsByRef.set(ref, []);
- }
- importsByRef.get(ref)!.push(component);
- });
-
- const importLines = Array.from(importsByRef.entries())
- .map(([ref, components]) => {
- const imports = components.join(', ');
- return `\n\timport { ${imports} } from '${ref}';`;
- })
- .join('');
-
- const scriptRegex = /', moduleScriptMatch.index) + ''.length;
- processedContent =
- processedContent.slice(0, moduleScriptEnd) +
- `\n\n` +
- processedContent.slice(moduleScriptEnd);
- } else {
- processedContent = `\n\n` + processedContent;
- }
- }
- }
-
- return {
- code: processedContent
- };
- }
- };
+ return liliCore(options);
}
export { lapikitPreprocess };
diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/compute-classes.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/compute-classes.test.ts
new file mode 100644
index 0000000..c5379cf
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/__test__/compute-classes.test.ts
@@ -0,0 +1,210 @@
+import { describe, it, expect } from 'vitest';
+import { computeSClasses } from '../mapped-code.js';
+import type { SClassProp } from '../types/index.js';
+
+describe('computeSClasses', () => {
+ describe('sClass as string', () => {
+ it('should return the string when sClass is a non-empty string', () => {
+ const result = computeSClasses('btn btn-primary', {});
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should return empty string when sClass is an empty string', () => {
+ const result = computeSClasses('', {});
+ expect(result).toBe('');
+ });
+ });
+
+ describe('sClass as array', () => {
+ it('should join array elements with spaces', () => {
+ const result = computeSClasses(['btn', 'btn-primary'], {});
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should filter out empty strings from array', () => {
+ const result = computeSClasses(['btn', '', 'btn-primary'], {});
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should filter out non-string values from array', () => {
+ const result = computeSClasses(
+ ['btn', null, undefined, 'btn-primary'] as unknown as SClassProp,
+ {}
+ );
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should return empty string for empty array', () => {
+ const result = computeSClasses([], {});
+ expect(result).toBe('');
+ });
+ });
+
+ describe('sClass as object', () => {
+ it('should add keys where value is true', () => {
+ const result = computeSClasses(
+ {
+ btn: true,
+ 'btn-primary': true,
+ 'btn-disabled': false
+ },
+ {}
+ );
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should use string values as class names', () => {
+ const result = computeSClasses(
+ {
+ btn: 'custom-button',
+ size: 'large-size'
+ },
+ {}
+ );
+ expect(result).toBe('custom-button large-size');
+ });
+
+ it('should handle mixed boolean and string values', () => {
+ const result = computeSClasses(
+ {
+ btn: true,
+ color: 'text-primary',
+ disabled: false
+ },
+ {}
+ );
+ expect(result).toBe('btn text-primary');
+ });
+
+ it('should filter out empty string values', () => {
+ const result = computeSClasses(
+ {
+ btn: true,
+ color: ''
+ },
+ {}
+ );
+ expect(result).toBe('btn');
+ });
+
+ it('should return empty string for empty object', () => {
+ const result = computeSClasses({}, {});
+ expect(result).toBe('');
+ });
+ });
+
+ describe('classDirectiveProps (s-class_xxx)', () => {
+ it('should add class name when value is true', () => {
+ const result = computeSClasses('', {
+ 's-class_btn': true,
+ 's-class_primary': true
+ });
+ expect(result).toBe('btn primary');
+ });
+
+ it('should concatenate base with string value', () => {
+ const result = computeSClasses('', {
+ 's-class_btn': '-primary',
+ 's-class_size': '-large'
+ });
+ expect(result).toBe('btn-primary size-large');
+ });
+
+ it('should ignore non-true and non-string values', () => {
+ const result = computeSClasses('', {
+ 's-class_btn': true,
+ 's-class_disabled': false,
+ 's-class_color': ''
+ });
+ expect(result).toBe('btn');
+ });
+
+ it('should handle mixed boolean and string values', () => {
+ const result = computeSClasses('', {
+ 's-class_btn': true,
+ 's-class_variant': '-outlined'
+ });
+ expect(result).toBe('btn variant-outlined');
+ });
+ });
+
+ describe('combined scenarios', () => {
+ it('should combine sClass string and classDirectiveProps', () => {
+ const result = computeSClasses('base-class', {
+ 's-class_modifier': '-active'
+ });
+ expect(result).toBe('base-class modifier-active');
+ });
+
+ it('should combine sClass array and classDirectiveProps', () => {
+ const result = computeSClasses(['btn', 'rounded'], {
+ 's-class_variant': '-primary'
+ });
+ expect(result).toBe('btn rounded variant-primary');
+ });
+
+ it('should combine sClass object and classDirectiveProps', () => {
+ const result = computeSClasses(
+ {
+ btn: true,
+ outlined: 'border-solid'
+ },
+ {
+ 's-class_color': '-blue'
+ }
+ );
+ expect(result).toBe('btn border-solid color-blue');
+ });
+
+ it('should handle all types combined', () => {
+ const result = computeSClasses(
+ {
+ btn: true,
+ size: 'text-large'
+ },
+ {
+ 's-class_variant': '-outlined',
+ 's-class_active': true
+ }
+ );
+ expect(result).toBe('btn text-large variant-outlined active');
+ });
+
+ it('should return empty string when everything is empty', () => {
+ const result = computeSClasses('', {});
+ expect(result).toBe('');
+ });
+
+ it('should handle null and undefined sClass', () => {
+ const result = computeSClasses(undefined, {});
+ expect(result).toBe('');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle multiple spaces in string sClass', () => {
+ const result = computeSClasses('btn btn-primary', {});
+ expect(result).toBe('btn btn-primary');
+ });
+
+ it('should preserve order of classes', () => {
+ const result = computeSClasses(['z-class', 'a-class'], {
+ 's-class_m': true
+ });
+ expect(result).toBe('z-class a-class m');
+ });
+
+ it('should handle special characters in class names', () => {
+ const result = computeSClasses(
+ {
+ 'btn--primary': true,
+ btn__icon: 'icon--hover'
+ },
+ {
+ 's-class_state': ':active'
+ }
+ );
+ expect(result).toBe('btn--primary icon--hover state:active');
+ });
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/compute-styles.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/compute-styles.test.ts
new file mode 100644
index 0000000..6144ffb
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/__test__/compute-styles.test.ts
@@ -0,0 +1,218 @@
+import { describe, it, expect } from 'vitest';
+import { computeSStyles } from '../mapped-code.js';
+
+describe('computeSStyles', () => {
+ describe('sStyle as object', () => {
+ it('should convert object to CSS style string', () => {
+ const result = computeSStyles(
+ {
+ color: 'red',
+ 'font-size': '16px'
+ },
+ {}
+ );
+ expect(result).toBe('color: red; font-size: 16px');
+ });
+
+ it('should filter out falsy values', () => {
+ const result = computeSStyles(
+ {
+ color: 'red',
+ display: false,
+ margin: ''
+ },
+ {}
+ );
+ expect(result).toBe('color: red');
+ });
+
+ it('should handle boolean true values', () => {
+ const result = computeSStyles(
+ {
+ color: 'blue',
+ visible: true
+ },
+ {}
+ );
+ expect(result).toBe('color: blue; visible: true');
+ });
+
+ it('should return empty string for empty object', () => {
+ const result = computeSStyles({}, {});
+ expect(result).toBe('');
+ });
+
+ it('should handle undefined sStyle', () => {
+ const result = computeSStyles(undefined, {});
+ expect(result).toBe('');
+ });
+
+ it('should handle CSS properties with units', () => {
+ const result = computeSStyles(
+ {
+ width: '100px',
+ height: '50%',
+ padding: '1rem'
+ },
+ {}
+ );
+ expect(result).toBe('width: 100px; height: 50%; padding: 1rem');
+ });
+ });
+
+ describe('styleDirectiveProps (s-style_xxx)', () => {
+ it('should add styles from directives', () => {
+ const result = computeSStyles(
+ {},
+ {
+ 's-style_color': 'red',
+ 's-style_background': 'blue'
+ }
+ );
+ expect(result).toBe('color: red; background: blue');
+ });
+
+ it('should filter out falsy directive values', () => {
+ const result = computeSStyles(
+ {},
+ {
+ 's-style_color': 'red',
+ 's-style_display': false,
+ 's-style_margin': ''
+ }
+ );
+ expect(result).toBe('color: red');
+ });
+
+ it('should handle boolean true values in directives', () => {
+ const result = computeSStyles(
+ {},
+ {
+ 's-style_visible': true,
+ 's-style_color': 'green'
+ }
+ );
+ expect(result).toBe('visible: true; color: green');
+ });
+
+ it('should handle hyphenated CSS properties', () => {
+ const result = computeSStyles(
+ {},
+ {
+ 's-style_font-size': '14px',
+ 's-style_text-align': 'center'
+ }
+ );
+ expect(result).toBe('font-size: 14px; text-align: center');
+ });
+ });
+
+ describe('combined scenarios', () => {
+ it('should combine sStyle object and styleDirectiveProps', () => {
+ const result = computeSStyles(
+ {
+ color: 'red',
+ 'font-size': '16px'
+ },
+ {
+ 's-style_margin': '10px',
+ 's-style_padding': '5px'
+ }
+ );
+ expect(result).toBe('color: red; font-size: 16px; margin: 10px; padding: 5px');
+ });
+
+ it('should filter falsy values from both sources', () => {
+ const result = computeSStyles(
+ {
+ color: 'red',
+ display: false
+ },
+ {
+ 's-style_margin': '10px',
+ 's-style_padding': ''
+ }
+ );
+ expect(result).toBe('color: red; margin: 10px');
+ });
+
+ it('should return empty string when everything is empty', () => {
+ const result = computeSStyles({}, {});
+ expect(result).toBe('');
+ });
+
+ it('should handle mixed boolean and string values', () => {
+ const result = computeSStyles(
+ {
+ color: 'blue',
+ visible: true
+ },
+ {
+ 's-style_display': 'block',
+ 's-style_active': true
+ }
+ );
+ expect(result).toBe('color: blue; visible: true; display: block; active: true');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should preserve order of styles', () => {
+ const result = computeSStyles(
+ {
+ 'z-index': '999',
+ 'a-property': 'value'
+ },
+ {
+ 's-style_m-property': 'test'
+ }
+ );
+ expect(result).toBe('z-index: 999; a-property: value; m-property: test');
+ });
+
+ it('should handle CSS custom properties (variables)', () => {
+ const result = computeSStyles(
+ {
+ '--primary-color': '#ff0000',
+ '--spacing': '1rem'
+ },
+ {}
+ );
+ expect(result).toBe('--primary-color: #ff0000; --spacing: 1rem');
+ });
+
+ it('should handle calc and other CSS functions', () => {
+ const result = computeSStyles(
+ {
+ width: 'calc(100% - 20px)',
+ transform: 'rotate(45deg)'
+ },
+ {}
+ );
+ expect(result).toBe('width: calc(100% - 20px); transform: rotate(45deg)');
+ });
+
+ it('should handle numeric values', () => {
+ const result = computeSStyles(
+ {
+ opacity: '0.5',
+ 'z-index': '10'
+ },
+ {}
+ );
+ expect(result).toBe('opacity: 0.5; z-index: 10');
+ });
+
+ it('should handle color formats', () => {
+ const result = computeSStyles(
+ {
+ color: 'rgb(255, 0, 0)',
+ background: '#ff0000',
+ border: 'rgba(0, 0, 0, 0.5)'
+ },
+ {}
+ );
+ expect(result).toBe('color: rgb(255, 0, 0); background: #ff0000; border: rgba(0, 0, 0, 0.5)');
+ });
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/compiler/__test__/make-component-props.test.ts b/packages/lapikit/src/lib/labs/compiler/__test__/make-component-props.test.ts
new file mode 100644
index 0000000..8567578
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/__test__/make-component-props.test.ts
@@ -0,0 +1,279 @@
+import { describe, it, expect } from 'vitest';
+import { makeComponentProps } from '../mapped-code.js';
+
+describe('makeComponentProps', () => {
+ describe('basic separation', () => {
+ it('should separate s-class_ directives into classProps', () => {
+ const props = {
+ 's-class_btn': true,
+ 's-class_variant': '-primary',
+ id: 'test',
+ disabled: false
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({
+ 's-class_btn': true,
+ 's-class_variant': '-primary'
+ });
+ });
+
+ it('should separate s-style_ directives into styleProps', () => {
+ const props = {
+ 's-style_color': 'red',
+ 's-style_font-size': '16px',
+ id: 'test',
+ disabled: false
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.styleProps).toEqual({
+ 's-style_color': 'red',
+ 's-style_font-size': '16px'
+ });
+ });
+
+ it('should separate remaining props into restProps', () => {
+ const props = {
+ 's-class_btn': true,
+ 's-style_color': 'red',
+ id: 'test',
+ disabled: false,
+ onClick: () => {}
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.restProps).toEqual({
+ id: 'test',
+ disabled: false,
+ onClick: expect.any(Function)
+ });
+ });
+ });
+
+ describe('s-class and s-style base properties', () => {
+ it('should exclude s-class base property from all groups', () => {
+ const props = {
+ 's-class': 'btn btn-primary',
+ 's-class_active': true,
+ id: 'test'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({
+ 's-class_active': true
+ });
+ expect(result.styleProps).toEqual({});
+ expect(result.restProps).toEqual({
+ id: 'test'
+ });
+ });
+
+ it('should exclude s-style base property from all groups', () => {
+ const props = {
+ 's-style': { color: 'red' },
+ 's-style_margin': '10px',
+ id: 'test'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({});
+ expect(result.styleProps).toEqual({
+ 's-style_margin': '10px'
+ });
+ expect(result.restProps).toEqual({
+ id: 'test'
+ });
+ });
+ });
+
+ describe('all types combined', () => {
+ it('should properly separate all three types of props', () => {
+ const props = {
+ 's-class': 'base-class',
+ 's-class_btn': true,
+ 's-class_variant': '-outlined',
+ 's-style': { display: 'flex' },
+ 's-style_color': 'blue',
+ 's-style_padding': '20px',
+ id: 'component',
+ disabled: false,
+ onClick: () => {},
+ 'data-testid': 'test'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({
+ 's-class_btn': true,
+ 's-class_variant': '-outlined'
+ });
+
+ expect(result.styleProps).toEqual({
+ 's-style_color': 'blue',
+ 's-style_padding': '20px'
+ });
+
+ expect(result.restProps).toEqual({
+ id: 'component',
+ disabled: false,
+ onClick: expect.any(Function),
+ 'data-testid': 'test'
+ });
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle empty props object', () => {
+ const result = makeComponentProps({});
+
+ expect(result.classProps).toEqual({});
+ expect(result.styleProps).toEqual({});
+ expect(result.restProps).toEqual({});
+ });
+
+ it('should handle props with only s-class directives', () => {
+ const props = {
+ 's-class_btn': true,
+ 's-class_size': '-large'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({
+ 's-class_btn': true,
+ 's-class_size': '-large'
+ });
+ expect(result.styleProps).toEqual({});
+ expect(result.restProps).toEqual({});
+ });
+
+ it('should handle props with only s-style directives', () => {
+ const props = {
+ 's-style_color': 'red',
+ 's-style_margin': '10px'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({});
+ expect(result.styleProps).toEqual({
+ 's-style_color': 'red',
+ 's-style_margin': '10px'
+ });
+ expect(result.restProps).toEqual({});
+ });
+
+ it('should handle props with only regular props', () => {
+ const props = {
+ id: 'test',
+ disabled: true,
+ 'aria-label': 'Button'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({});
+ expect(result.styleProps).toEqual({});
+ expect(result.restProps).toEqual({
+ id: 'test',
+ disabled: true,
+ 'aria-label': 'Button'
+ });
+ });
+
+ it('should handle hyphenated s-class directives', () => {
+ const props = {
+ 's-class_btn-primary': true,
+ 's-class_font-size': '-large'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.classProps).toEqual({
+ 's-class_btn-primary': true,
+ 's-class_font-size': '-large'
+ });
+ });
+
+ it('should handle hyphenated s-style directives', () => {
+ const props = {
+ 's-style_font-size': '14px',
+ 's-style_background-color': 'white'
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.styleProps).toEqual({
+ 's-style_font-size': '14px',
+ 's-style_background-color': 'white'
+ });
+ });
+
+ it('should preserve values of different types', () => {
+ const fn = () => {};
+ const obj = { nested: 'value' };
+
+ const props = {
+ 's-class_active': true,
+ 's-style_display': 'block',
+ string: 'text',
+ number: 42,
+ boolean: true,
+ null: null,
+ undefined: undefined,
+ function: fn,
+ object: obj,
+ array: [1, 2, 3]
+ };
+
+ const result = makeComponentProps(props);
+
+ expect(result.restProps).toEqual({
+ string: 'text',
+ number: 42,
+ boolean: true,
+ null: null,
+ undefined: undefined,
+ function: fn,
+ object: obj,
+ array: [1, 2, 3]
+ });
+ });
+ });
+
+ describe('return object structure', () => {
+ it('should always return an object with classProps, styleProps, and restProps keys', () => {
+ const result = makeComponentProps({ test: 'value' });
+
+ expect(result).toHaveProperty('classProps');
+ expect(result).toHaveProperty('styleProps');
+ expect(result).toHaveProperty('restProps');
+ });
+
+ it('should return separate objects that do not reference the original props', () => {
+ const props = {
+ 's-class_btn': true,
+ 's-style_color': 'red',
+ id: 'test'
+ };
+
+ const result = makeComponentProps(props);
+
+ // Modify result objects
+ result.classProps['s-class_new'] = true;
+ result.styleProps['s-style_new'] = 'blue';
+ result.restProps['newProp'] = 'value';
+
+ // Original should be unchanged
+ expect(props).not.toHaveProperty('s-class_new');
+ expect(props).not.toHaveProperty('s-style_new');
+ expect(props).not.toHaveProperty('newProp');
+ });
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/compiler/components.ts b/packages/lapikit/src/lib/labs/compiler/components.ts
new file mode 100644
index 0000000..90e2eb9
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/components.ts
@@ -0,0 +1,3 @@
+const lapikitComponents: readonly string[] = ['sheet'] as const;
+
+export default lapikitComponents;
diff --git a/packages/lapikit/src/lib/labs/compiler/index.ts b/packages/lapikit/src/lib/labs/compiler/index.ts
new file mode 100644
index 0000000..7a5df06
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/index.ts
@@ -0,0 +1 @@
+export * from './preprocess/index.js';
diff --git a/packages/lapikit/src/lib/labs/compiler/mapped-code.ts b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts
new file mode 100644
index 0000000..a9ce106
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/mapped-code.ts
@@ -0,0 +1,125 @@
+import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/index.js';
+import type { PropValue } from '$lib/labs/utils/types/index.js';
+
+/**
+ * Computes a string of class names based on the provided sClass and classDirectiveProps.
+ * @param sClass - The s-class property which can be a string, array, or object.
+ * @param classDirectiveProps - An object containing s-class_xxx directives.
+ * @returns A string of class names.
+ */
+export function computeSClasses(
+ sClass: SClassProp,
+ classDirectiveProps: Record
+): string {
+ const classes: string[] = [];
+
+ // s-class string
+ if (typeof sClass === 'string' && sClass) {
+ classes.push(sClass);
+ }
+
+ // s-class array
+ if (Array.isArray(sClass)) {
+ for (const value of sClass) {
+ if (typeof value === 'string' && value) {
+ classes.push(value);
+ }
+ }
+ }
+
+ // s-class object
+ if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) {
+ const entries = Object.entries(sClass);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ if (value === true) {
+ classes.push(key);
+ } else if (typeof value === 'string' && value) {
+ classes.push(value);
+ }
+ }
+ }
+ }
+
+ // s-class_xxx
+ const classEntries = Object.entries(classDirectiveProps);
+ if (classEntries.length > 0) {
+ for (const [key, value] of classEntries) {
+ // Use slice instead of replace for better performance (8 = 's-class_'.length)
+ const base = key.slice(8);
+
+ if (value === true) {
+ classes.push(base);
+ } else if (typeof value === 'string' && value) {
+ classes.push(`${base}${value}`);
+ }
+ }
+ }
+
+ return classes.join(' ');
+}
+
+/**
+ * Computes a string of style declarations based on the provided sStyle and styleDirectiveProps.
+ * @param sStyle - The s-style property which is an object of style key-value pairs.
+ * @param styleDirectiveProps - An object containing s-style_xxx directives.
+ * @returns A string of style declarations.
+ */
+export function computeSStyles(
+ sStyle: SStyleProp,
+ styleDirectiveProps: Record
+): string {
+ const styles: string[] = [];
+
+ if (sStyle && typeof sStyle === 'object') {
+ const entries = Object.entries(sStyle);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ if (value) {
+ styles.push(`${key}: ${value}`);
+ }
+ }
+ }
+ }
+
+ const styleEntries = Object.entries(styleDirectiveProps);
+ if (styleEntries.length > 0) {
+ for (const [key, value] of styleEntries) {
+ // Use slice instead of replace for better performance (8 = 's-style_'.length)
+ const base = key.slice(8);
+ if (value) {
+ styles.push(`${base}: ${value}`);
+ }
+ }
+ }
+
+ return styles.join('; ');
+}
+
+/**
+ * Makes component props by separating s-class and s-style directives from other props.
+ * Optimized to use a single pass instead of three separate iterations.
+ * @param props The original props object containing all props.
+ * @returns An object containing separated classProps, styleProps, and restProps.
+ */
+export function makeComponentProps(props: Record): {
+ classProps: Record;
+ styleProps: Record;
+ restProps: Record;
+} {
+ const classProps: Record = {};
+ const styleProps: Record = {};
+ const restProps: Record = {};
+
+ for (const [key, value] of Object.entries(props)) {
+ if (key.startsWith('s-class_')) {
+ classProps[key] = value as PropValue;
+ } else if (key.startsWith('s-style_')) {
+ styleProps[key] = value as PropValue;
+ } else if (!key.startsWith('s-class') && !key.startsWith('s-style')) {
+ restProps[key] = value;
+ }
+ }
+
+ return { classProps, styleProps, restProps };
+}
diff --git a/packages/lapikit/src/lib/labs/compiler/plugins.ts b/packages/lapikit/src/lib/labs/compiler/plugins.ts
new file mode 100644
index 0000000..9b5e1d0
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/plugins.ts
@@ -0,0 +1,8 @@
+const lapikitPlugins = {
+ repl: {
+ components: ['repl'],
+ ref: '@lapikit/repl'
+ }
+} as const;
+
+export default lapikitPlugins;
diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts
new file mode 100644
index 0000000..be81e14
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/preprocess/__test__/lili.test.ts
@@ -0,0 +1,41 @@
+import { describe, it, expect } from 'vitest';
+import { liliCore } from '../lili.js';
+import { componentName, lapikitImportsRef } from '../source-import.js';
+
+describe('liliCore', () => {
+ it('converts kit tags with multiline attributes and injects imports', () => {
+ const preprocess = liliCore();
+ const sheetName = componentName('sheet');
+ const input = `\n\n a > b}\n/>`;
+
+ const result = preprocess.markup({ content: input });
+
+ expect(result?.code).toContain(`import { ${sheetName} } from '${lapikitImportsRef}';`);
+ expect(result?.code).toContain(`<${sheetName}`);
+ expect(result?.code).toContain('class="gnome"');
+ expect(result?.code).toContain('on:click={() => a > b}');
+ expect(result?.code).not.toContain(' {
+ const preprocess = liliCore();
+ const sheetName = componentName('sheet');
+ const input = `\n\n\n\ncontent`;
+
+ const result = preprocess.markup({ content: input });
+
+ expect(result?.code).toContain("const template = '';");
+ expect(result?.code).toContain('content: "";');
+ expect(result?.code).toContain(`<${sheetName}>content${sheetName}>`);
+ expect(result?.code).toContain(`import { ${sheetName} } from '${lapikitImportsRef}';`);
+ });
+
+ it('does nothing when kit tags exist only inside script or style', () => {
+ const preprocess = liliCore();
+ const input = `\n\n`;
+
+ const result = preprocess.markup({ content: input });
+
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/decode-sourcemap.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/decode-sourcemap.ts
new file mode 100644
index 0000000..e558cb1
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/preprocess/decode-sourcemap.ts
@@ -0,0 +1,128 @@
+import { findTagEnd } from '$lib/labs/compiler/preprocess/finding.js';
+import type { ComponentInfo, KitComponentScan } from '$lib/labs/compiler/types/index.js';
+
+export const decodeSourceMap = (
+ content: string,
+ componentInfo: Map
+): KitComponentScan => {
+ const importedComponents = new Map();
+ const parts: string[] = [];
+ let changed = false;
+ let index = 0;
+ const length = content.length;
+
+ while (index < length) {
+ const nextLt = content.indexOf('<', index);
+ if (nextLt === -1) {
+ parts.push(content.slice(index));
+ break;
+ }
+
+ if (nextLt > index) {
+ parts.push(content.slice(index, nextLt));
+ }
+
+ const ch = content[nextLt + 1];
+ const isClosing = ch === '/';
+
+ if (content.startsWith('', moduleScriptMatch.index) + ''.length;
+ processedContent =
+ processedContent.slice(0, moduleScriptEnd) +
+ `\n\n` +
+ processedContent.slice(moduleScriptEnd);
+ } else {
+ processedContent = `\n\n` + processedContent;
+ }
+ }
+ }
+
+ return {
+ code: processedContent
+ };
+ }
+ };
+}
diff --git a/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts b/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts
new file mode 100644
index 0000000..204150a
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/preprocess/source-import.ts
@@ -0,0 +1,10 @@
+/**
+ * componentName generates the component name used in imports
+ * @param shortName The short name of the component
+ * @returns The component name with "Kit" prefix and the first letter capitalized
+ */
+export function componentName(shortName: string): string {
+ return 'Kit' + shortName.charAt(0).toUpperCase() + shortName.slice(1);
+}
+
+export const lapikitImportsRef = 'lapikit/labs/components';
diff --git a/packages/lapikit/src/lib/labs/compiler/types/dom.ts b/packages/lapikit/src/lib/labs/compiler/types/dom.ts
new file mode 100644
index 0000000..3676b7f
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/types/dom.ts
@@ -0,0 +1,2 @@
+export type SClassProp = string | string[] | Record | undefined;
+export type SStyleProp = Record | undefined;
diff --git a/packages/lapikit/src/lib/labs/compiler/types/index.ts b/packages/lapikit/src/lib/labs/compiler/types/index.ts
new file mode 100644
index 0000000..f9436cd
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/types/index.ts
@@ -0,0 +1,2 @@
+export type * from './dom.js';
+export * from './options.js';
diff --git a/packages/lapikit/src/lib/labs/compiler/types/options.ts b/packages/lapikit/src/lib/labs/compiler/types/options.ts
new file mode 100644
index 0000000..8d70dff
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/compiler/types/options.ts
@@ -0,0 +1,14 @@
+export type LapikitPreprocessOptions = {
+ plugins?: string[];
+};
+
+export type KitComponentScan = {
+ code: string;
+ changed: boolean;
+ importedComponents: Map;
+};
+
+export type ComponentInfo = {
+ name: string;
+ ref: string;
+};
diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte b/packages/lapikit/src/lib/labs/components/btn/btn.svelte
deleted file mode 100644
index 3319b5e..0000000
--- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
diff --git a/packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts b/packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts
deleted file mode 100644
index 00f70a0..0000000
--- a/packages/lapikit/src/lib/labs/components/btn/btn.svelte.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import type { SClassProp, SStyleProp } from '../../utils/index.js';
-
-export function useClassName({
- baseClass = '',
- className,
- sClass,
- classDirectiveProps
-}: {
- baseClass?: string;
- className?: string;
- sClass?: SClassProp;
- classDirectiveProps?: Record;
-} = {}) {
- return {
- get value() {
- const classes: string[] = [];
-
- if (baseClass) {
- classes.push(baseClass);
- }
-
- if (typeof sClass === 'string' && sClass) {
- classes.push(sClass);
- }
-
- if (Array.isArray(sClass)) {
- for (const value of sClass) {
- if (typeof value === 'string' && value) {
- classes.push(value);
- }
- }
- }
-
- if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) {
- Object.entries(sClass).forEach(([key, value]) => {
- if (value === true) {
- classes.push(key);
- } else if (typeof value === 'string' && value) {
- classes.push(value);
- }
- });
- }
-
- if (classDirectiveProps) {
- Object.entries(classDirectiveProps).forEach(([key, value]) => {
- const base = key.replace('s-class_', '');
-
- if (value === true) {
- classes.push(base);
- } else if (typeof value === 'string' && value) {
- classes.push(`${base}${value}`);
- }
- });
- }
-
- if (className) {
- classes.push(className);
- }
-
- return classes.filter(Boolean).join(' ');
- }
- };
-}
-
-export function useStyles({
- styleAttr,
- sStyle,
- styleDirectiveProps
-}: {
- styleAttr?: string;
- sStyle?: SStyleProp;
- styleDirectiveProps?: Record;
-} = {}) {
- return {
- get value() {
- const styles: string[] = [];
-
- if (sStyle && typeof sStyle === 'object') {
- Object.entries(sStyle).forEach(([key, value]) => {
- if (value) {
- styles.push(`${key}: ${value}`);
- }
- });
- }
-
- if (styleDirectiveProps) {
- Object.entries(styleDirectiveProps).forEach(([key, value]) => {
- const base = key.replace('s-style_', '');
- if (value) {
- styles.push(`${base}: ${value}`);
- }
- });
- }
-
- if (styleAttr) {
- styles.push(styleAttr);
- }
-
- return styles.filter(Boolean).join('; ');
- }
- };
-}
diff --git a/packages/lapikit/src/lib/labs/components/index.ts b/packages/lapikit/src/lib/labs/components/index.ts
index 5139ff9..f82ea47 100644
--- a/packages/lapikit/src/lib/labs/components/index.ts
+++ b/packages/lapikit/src/lib/labs/components/index.ts
@@ -1,2 +1,2 @@
// components
-export { default as KitBtn } from './btn/btn.svelte';
+export { default as KitSheet } from './sheet/sheet.svelte';
diff --git a/packages/lapikit/src/lib/labs/components/sheet/sheet.svelte b/packages/lapikit/src/lib/labs/components/sheet/sheet.svelte
new file mode 100644
index 0000000..298bad5
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/components/sheet/sheet.svelte
@@ -0,0 +1,49 @@
+
+
+
+ {@render children()}
+
+
+
diff --git a/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts b/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts
new file mode 100644
index 0000000..9b3f1bf
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/utils/__test__/use-class-name.test.ts
@@ -0,0 +1,241 @@
+import { describe, expect, it } from 'vitest';
+import { useClassName } from '../components.js';
+
+describe('useClassName', () => {
+ describe('baseClass', () => {
+ it('should return baseClass when only baseClass is provided', () => {
+ const result = useClassName({ baseClass: 'btn' });
+ expect(result).toBe('btn');
+ });
+
+ it('should return empty string when no arguments are provided', () => {
+ const result = useClassName();
+ expect(result).toBe('');
+ });
+
+ it('should ignore empty baseClass', () => {
+ const result = useClassName({ baseClass: '' });
+ expect(result).toBe('');
+ });
+ });
+
+ describe('className', () => {
+ it('should return className when only className is provided', () => {
+ const result = useClassName({ className: 'custom-class' });
+ expect(result).toBe('custom-class');
+ });
+
+ it('should combine baseClass and className', () => {
+ const result = useClassName({ baseClass: 'btn', className: 'custom' });
+ expect(result).toBe('btn custom');
+ });
+
+ it('should ignore empty className', () => {
+ const result = useClassName({ baseClass: 'btn', className: '' });
+ expect(result).toBe('btn');
+ });
+ });
+
+ describe('sClass - string', () => {
+ it('should handle sClass as a string', () => {
+ const result = useClassName({ sClass: 'primary' });
+ expect(result).toBe('primary');
+ });
+
+ it('should combine baseClass and sClass string', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: 'primary' });
+ expect(result).toBe('btn primary');
+ });
+
+ it('should ignore empty sClass string', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: '' });
+ expect(result).toBe('btn');
+ });
+ });
+
+ describe('sClass - array', () => {
+ it('should handle sClass as an array of strings', () => {
+ const result = useClassName({ sClass: ['primary', 'large'] });
+ expect(result).toBe('primary large');
+ });
+
+ it('should combine baseClass and sClass array', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: ['primary', 'large'] });
+ expect(result).toBe('btn primary large');
+ });
+
+ it('should filter out empty strings in array', () => {
+ const result = useClassName({ sClass: ['primary', '', 'large', ''] });
+ expect(result).toBe('primary large');
+ });
+
+ it('should handle empty array', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: [] });
+ expect(result).toBe('btn');
+ });
+
+ it('should ignore non-string values in array', () => {
+ // Testing runtime behavior with invalid types
+ const result = useClassName({
+ sClass: ['primary', null, undefined, 123, true] as unknown as string[]
+ });
+ expect(result).toBe('primary');
+ });
+ });
+
+ describe('sClass - object', () => {
+ it('should handle sClass object with boolean true values', () => {
+ const result = useClassName({ sClass: { primary: true, disabled: true } });
+ expect(result).toBe('primary disabled');
+ });
+
+ it('should ignore sClass object with boolean false values', () => {
+ const result = useClassName({ sClass: { primary: true, disabled: false } });
+ expect(result).toBe('primary');
+ });
+
+ it('should handle sClass object with string values', () => {
+ const result = useClassName({ sClass: { variant: 'primary', size: 'lg' } });
+ expect(result).toBe('primary lg');
+ });
+
+ it('should ignore empty string values in object', () => {
+ const result = useClassName({ sClass: { variant: 'primary', size: '' } });
+ expect(result).toBe('primary');
+ });
+
+ it('should combine baseClass and sClass object', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: { primary: true, large: true } });
+ expect(result).toBe('btn primary large');
+ });
+
+ it('should handle empty object', () => {
+ const result = useClassName({ baseClass: 'btn', sClass: {} });
+ expect(result).toBe('btn');
+ });
+
+ it('should handle mixed boolean and string values in object', () => {
+ const result = useClassName({
+ sClass: { primary: true, variant: 'outlined', disabled: false, size: 'md' }
+ });
+ expect(result).toBe('primary outlined md');
+ });
+ });
+
+ describe('classProps (s-class_xxx directives)', () => {
+ it('should handle classProps with boolean true', () => {
+ const result = useClassName({ classProps: { 's-class_active': true } });
+ expect(result).toBe('active');
+ });
+
+ it('should handle classProps with string values', () => {
+ const result = useClassName({ classProps: { 's-class_variant': '-primary' } });
+ expect(result).toBe('variant-primary');
+ });
+
+ it('should ignore classProps with boolean false', () => {
+ const result = useClassName({
+ classProps: { 's-class_active': true, 's-class_disabled': false }
+ });
+ expect(result).toBe('active');
+ });
+
+ it('should handle multiple classProps', () => {
+ const result = useClassName({
+ classProps: {
+ 's-class_active': true,
+ 's-class_variant': '-primary',
+ 's-class_size': '-lg'
+ }
+ });
+ expect(result).toBe('active variant-primary size-lg');
+ });
+
+ it('should handle empty classProps object', () => {
+ const result = useClassName({ baseClass: 'btn', classProps: {} });
+ expect(result).toBe('btn');
+ });
+
+ it('should ignore empty string values in classProps', () => {
+ const result = useClassName({
+ classProps: { 's-class_variant': '-primary', 's-class_size': '' }
+ });
+ expect(result).toBe('variant-primary');
+ });
+ });
+
+ describe('combination of all props', () => {
+ it('should combine all props in correct order', () => {
+ const result = useClassName({
+ baseClass: 'btn',
+ sClass: 'primary',
+ className: 'custom',
+ classProps: { 's-class_active': true }
+ });
+ expect(result).toBe('btn primary active custom');
+ });
+
+ it('should handle complex combination with sClass array', () => {
+ const result = useClassName({
+ baseClass: 'btn',
+ sClass: ['primary', 'large'],
+ className: 'my-btn',
+ classProps: { 's-class_rounded': true, 's-class_shadow': '-md' }
+ });
+ expect(result).toBe('btn primary large rounded shadow-md my-btn');
+ });
+
+ it('should handle complex combination with sClass object', () => {
+ const result = useClassName({
+ baseClass: 'btn',
+ sClass: { primary: true, disabled: false, variant: 'outlined' },
+ className: 'custom',
+ classProps: { 's-class_active': true }
+ });
+ expect(result).toBe('btn primary outlined active custom');
+ });
+
+ it('should filter out all falsy values', () => {
+ const result = useClassName({
+ baseClass: '',
+ sClass: { primary: false, active: true, size: '' },
+ className: '',
+ classProps: { 's-class_disabled': false, 's-class_rounded': true }
+ });
+ expect(result).toBe('active rounded');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle undefined values gracefully', () => {
+ const result = useClassName({
+ baseClass: undefined,
+ sClass: undefined,
+ className: undefined,
+ classProps: undefined
+ });
+ expect(result).toBe('');
+ });
+
+ it('should handle null values in sClass object', () => {
+ // Testing runtime behavior with invalid types
+ const result = useClassName({
+ sClass: { primary: null } as unknown as Record
+ });
+ expect(result).toBe('');
+ });
+
+ it('should handle number values in classProps', () => {
+ // Testing runtime behavior with invalid types
+ const result = useClassName({
+ classProps: { 's-class_count': 5 } as unknown as Record
+ });
+ expect(result).toBe('');
+ });
+
+ it('should preserve whitespace in class names', () => {
+ const result = useClassName({ baseClass: 'btn', className: 'my custom' });
+ expect(result).toBe('btn my custom');
+ });
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts b/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts
new file mode 100644
index 0000000..aa0461d
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/utils/__test__/use-styles.test.ts
@@ -0,0 +1,260 @@
+import { describe, expect, it } from 'vitest';
+import { useStyles } from '../components.js';
+
+describe('useStyles', () => {
+ describe('no arguments', () => {
+ it('should return empty string when no arguments are provided', () => {
+ const result = useStyles();
+ expect(result).toBe('');
+ });
+
+ it('should return empty string when all arguments are undefined', () => {
+ const result = useStyles({
+ styleAttr: undefined,
+ sStyle: undefined,
+ styleProps: undefined
+ });
+ expect(result).toBe('');
+ });
+ });
+
+ describe('styleAttr', () => {
+ it('should return styleAttr when only styleAttr is provided', () => {
+ const result = useStyles({ styleAttr: 'color: red' });
+ expect(result).toBe('color: red');
+ });
+
+ it('should ignore empty styleAttr', () => {
+ const result = useStyles({ styleAttr: '' });
+ expect(result).toBe('');
+ });
+
+ it('should handle multiple style declarations in styleAttr', () => {
+ const result = useStyles({ styleAttr: 'color: red; font-size: 16px' });
+ expect(result).toBe('color: red; font-size: 16px');
+ });
+ });
+
+ describe('sStyle - object', () => {
+ it('should handle sStyle object with string values', () => {
+ const result = useStyles({ sStyle: { color: 'red', fontSize: '16px' } });
+ expect(result).toBe('color: red; fontSize: 16px');
+ });
+
+ it('should handle sStyle object with single property', () => {
+ const result = useStyles({ sStyle: { color: 'blue' } });
+ expect(result).toBe('color: blue');
+ });
+
+ it('should ignore falsy values in sStyle object', () => {
+ const result = useStyles({
+ sStyle: { color: 'red', fontSize: '', display: false as unknown as string }
+ });
+ expect(result).toBe('color: red');
+ });
+
+ it('should handle empty sStyle object', () => {
+ const result = useStyles({ sStyle: {} });
+ expect(result).toBe('');
+ });
+
+ it('should handle sStyle with boolean true values', () => {
+ const result = useStyles({
+ sStyle: { color: 'red', display: true as unknown as string }
+ });
+ expect(result).toBe('color: red; display: true');
+ });
+
+ it('should handle CSS custom properties (variables)', () => {
+ const result = useStyles({
+ sStyle: { '--primary-color': '#3b82f6', '--spacing': '1rem' }
+ });
+ expect(result).toBe('--primary-color: #3b82f6; --spacing: 1rem');
+ });
+
+ it('should handle numeric string values', () => {
+ const result = useStyles({ sStyle: { width: '100px', height: '50px' } });
+ expect(result).toBe('width: 100px; height: 50px');
+ });
+ });
+
+ describe('styleProps (s-style_xxx directives)', () => {
+ it('should handle styleProps with string values', () => {
+ const result = useStyles({ styleProps: { 's-style_color': 'red' } });
+ expect(result).toBe('color: red');
+ });
+
+ it('should handle multiple styleProps', () => {
+ const result = useStyles({
+ styleProps: {
+ 's-style_color': 'red',
+ 's-style_fontSize': '16px',
+ 's-style_display': 'flex'
+ }
+ });
+ expect(result).toBe('color: red; fontSize: 16px; display: flex');
+ });
+
+ it('should ignore falsy values in styleProps', () => {
+ const result = useStyles({
+ styleProps: {
+ 's-style_color': 'red',
+ 's-style_fontSize': '',
+ 's-style_display': false as unknown as string
+ }
+ });
+ expect(result).toBe('color: red');
+ });
+
+ it('should handle empty styleProps object', () => {
+ const result = useStyles({ styleProps: {} });
+ expect(result).toBe('');
+ });
+
+ it('should handle styleProps with CSS custom properties', () => {
+ const result = useStyles({
+ styleProps: {
+ 's-style_--primary': '#3b82f6',
+ 's-style_--spacing': '1rem'
+ }
+ });
+ expect(result).toBe('--primary: #3b82f6; --spacing: 1rem');
+ });
+
+ it('should handle styleProps with boolean true', () => {
+ const result = useStyles({
+ styleProps: { 's-style_display': true as unknown as string }
+ });
+ expect(result).toBe('display: true');
+ });
+ });
+
+ describe('combination of props', () => {
+ it('should combine sStyle and styleAttr', () => {
+ const result = useStyles({
+ sStyle: { color: 'red' },
+ styleAttr: 'font-size: 16px'
+ });
+ expect(result).toBe('color: red; font-size: 16px');
+ });
+
+ it('should combine styleProps and styleAttr', () => {
+ const result = useStyles({
+ styleProps: { 's-style_color': 'red' },
+ styleAttr: 'font-size: 16px'
+ });
+ expect(result).toBe('color: red; font-size: 16px');
+ });
+
+ it('should combine all props in correct order (sStyle, styleProps, styleAttr)', () => {
+ const result = useStyles({
+ sStyle: { color: 'red' },
+ styleProps: { 's-style_fontSize': '16px' },
+ styleAttr: 'display: flex'
+ });
+ expect(result).toBe('color: red; fontSize: 16px; display: flex');
+ });
+
+ it('should handle complex combination with multiple properties', () => {
+ const result = useStyles({
+ sStyle: { color: 'red', backgroundColor: 'white' },
+ styleProps: {
+ 's-style_fontSize': '16px',
+ 's-style_padding': '10px'
+ },
+ styleAttr: 'display: flex; align-items: center'
+ });
+ expect(result).toBe(
+ 'color: red; backgroundColor: white; fontSize: 16px; padding: 10px; display: flex; align-items: center'
+ );
+ });
+
+ it('should filter out all falsy values in combination', () => {
+ const result = useStyles({
+ sStyle: { color: 'red', fontSize: '' },
+ styleProps: { 's-style_display': 'flex', 's-style_margin': '' },
+ styleAttr: ''
+ });
+ expect(result).toBe('color: red; display: flex');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle null sStyle', () => {
+ const result = useStyles({ sStyle: null as unknown as Record });
+ expect(result).toBe('');
+ });
+
+ it('should handle undefined styleProps', () => {
+ const result = useStyles({ styleProps: undefined });
+ expect(result).toBe('');
+ });
+
+ it('should handle number values in sStyle', () => {
+ // Testing runtime behavior with invalid types
+ const result = useStyles({
+ sStyle: { width: 100 as unknown as string, color: 'red' }
+ });
+ expect(result).toBe('width: 100; color: red');
+ });
+
+ it('should handle null values in sStyle object', () => {
+ // Testing runtime behavior with invalid types
+ const result = useStyles({
+ sStyle: { color: null as unknown as string, fontSize: '16px' }
+ });
+ expect(result).toBe('fontSize: 16px');
+ });
+
+ it('should preserve semicolons in styleAttr', () => {
+ const result = useStyles({
+ sStyle: { color: 'red' },
+ styleAttr: 'font-size: 16px;'
+ });
+ expect(result).toBe('color: red; font-size: 16px;');
+ });
+
+ it('should handle styleAttr with leading/trailing spaces', () => {
+ const result = useStyles({ styleAttr: ' color: red ' });
+ expect(result).toBe(' color: red ');
+ });
+
+ it('should handle empty string values correctly', () => {
+ const result = useStyles({
+ sStyle: { color: '', fontSize: '16px' },
+ styleProps: { 's-style_display': '' }
+ });
+ expect(result).toBe('fontSize: 16px');
+ });
+
+ it('should handle zero values in sStyle', () => {
+ const result = useStyles({
+ sStyle: { margin: '0', padding: '0px', opacity: '0' }
+ });
+ expect(result).toBe('margin: 0; padding: 0px; opacity: 0');
+ });
+ });
+
+ describe('CSS properties formatting', () => {
+ it('should handle kebab-case properties', () => {
+ const result = useStyles({
+ sStyle: { 'background-color': 'red', 'font-size': '16px' }
+ });
+ expect(result).toBe('background-color: red; font-size: 16px');
+ });
+
+ it('should handle camelCase properties', () => {
+ const result = useStyles({
+ sStyle: { backgroundColor: 'red', fontSize: '16px' }
+ });
+ expect(result).toBe('backgroundColor: red; fontSize: 16px');
+ });
+
+ it('should handle mixed kebab-case and camelCase', () => {
+ const result = useStyles({
+ sStyle: { 'background-color': 'red', fontSize: '16px' }
+ });
+ expect(result).toBe('background-color: red; fontSize: 16px');
+ });
+ });
+});
diff --git a/packages/lapikit/src/lib/labs/utils/components.ts b/packages/lapikit/src/lib/labs/utils/components.ts
new file mode 100644
index 0000000..a4cbefe
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/utils/components.ts
@@ -0,0 +1,110 @@
+import type { useClassNameProps, useStylesProps } from '$lib/labs/utils/types/index.js';
+
+/**
+ * useClassName - Utility to compute class names for a component.
+ * @param baseClass - The base class name for the component.
+ * @param className - Additional class names as a string.
+ * @param sClass - The s-class property which can be a string, array, or object.
+ * @param classProps - An object containing s-class_xxx directives.
+ * @returns A computed class string.
+ */
+export function useClassName({
+ baseClass = '',
+ className,
+ sClass,
+ classProps
+}: useClassNameProps = {}): string {
+ const classes: string[] = [];
+
+ if (baseClass) {
+ classes.push(baseClass);
+ }
+
+ if (typeof sClass === 'string' && sClass) {
+ classes.push(sClass);
+ }
+
+ if (Array.isArray(sClass)) {
+ for (const value of sClass) {
+ if (typeof value === 'string' && value) {
+ classes.push(value);
+ }
+ }
+ }
+
+ if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) {
+ const entries = Object.entries(sClass);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ if (value === true) {
+ classes.push(key);
+ } else if (typeof value === 'string' && value) {
+ classes.push(value);
+ }
+ }
+ }
+ }
+
+ if (classProps) {
+ const entries = Object.entries(classProps);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ // Use slice instead of replace for better performance (8 = 's-class_'.length)
+ const base = key.slice(8);
+
+ if (value === true) {
+ classes.push(base);
+ } else if (typeof value === 'string' && value) {
+ classes.push(`${base}${value}`);
+ }
+ }
+ }
+ }
+
+ if (className) {
+ classes.push(className);
+ }
+
+ return classes.filter(Boolean).join(' ');
+}
+
+/**
+ * useStyles - Utility to compute style declarations for a component (optimized pure function).
+ * @param styleAttr - Inline style attribute as a string.
+ * @param sStyle - The s-style property which is an object of style key-value pairs.
+ * @param styleProps - An object containing s-style_xxx directives.
+ * @returns A computed style string.
+ */
+export function useStyles({ styleAttr, sStyle, styleProps }: useStylesProps = {}): string {
+ const styles: string[] = [];
+
+ if (sStyle && typeof sStyle === 'object') {
+ const entries = Object.entries(sStyle);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ if (value) {
+ styles.push(`${key}: ${value}`);
+ }
+ }
+ }
+ }
+
+ if (styleProps) {
+ const entries = Object.entries(styleProps);
+ if (entries.length > 0) {
+ for (const [key, value] of entries) {
+ // Use slice instead of replace for better performance (8 = 's-style_'.length)
+ const base = key.slice(8);
+ if (value) {
+ styles.push(`${base}: ${value}`);
+ }
+ }
+ }
+ }
+
+ if (styleAttr) {
+ styles.push(styleAttr);
+ }
+
+ return styles.filter(Boolean).join('; ');
+}
diff --git a/packages/lapikit/src/lib/labs/utils/index.ts b/packages/lapikit/src/lib/labs/utils/index.ts
index 1b692d7..dcd948a 100644
--- a/packages/lapikit/src/lib/labs/utils/index.ts
+++ b/packages/lapikit/src/lib/labs/utils/index.ts
@@ -1 +1 @@
-export * from './props.js';
+export * from './components.js';
diff --git a/packages/lapikit/src/lib/labs/utils/props.ts b/packages/lapikit/src/lib/labs/utils/props.ts
deleted file mode 100644
index c3cf22a..0000000
--- a/packages/lapikit/src/lib/labs/utils/props.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-export type SClassProp = string | string[] | Record | undefined;
-export type SStyleProp = Record | undefined;
-
-export function splitSyntheticProps(allProps: Record) {
- const classDirectiveProps = Object.fromEntries(
- Object.entries(allProps).filter(([key]) => key.startsWith('s-class_'))
- );
-
- const styleDirectiveProps = Object.fromEntries(
- Object.entries(allProps).filter(([key]) => key.startsWith('s-style_'))
- );
-
- const regularProps = Object.fromEntries(
- Object.entries(allProps).filter(
- ([key]) => !key.startsWith('s-class') && !key.startsWith('s-style')
- )
- );
-
- return { classDirectiveProps, styleDirectiveProps, regularProps };
-}
-
-export function computeSClasses(
- sClass: SClassProp,
- classDirectiveProps: Record
-): string {
- const classes: string[] = [];
-
- // s-class string
- if (typeof sClass === 'string' && sClass) {
- classes.push(sClass);
- }
-
- // s-class array
- if (Array.isArray(sClass)) {
- for (const value of sClass) {
- if (typeof value === 'string' && value) {
- classes.push(value);
- }
- }
- }
-
- // s-class object
- if (sClass && typeof sClass === 'object' && !Array.isArray(sClass)) {
- Object.entries(sClass).forEach(([key, value]) => {
- if (value === true) {
- classes.push(key);
- } else if (typeof value === 'string' && value) {
- classes.push(value);
- }
- });
- }
-
- // s-class_xxx
- Object.entries(classDirectiveProps).forEach(([key, value]) => {
- const base = key.replace('s-class_', '');
-
- if (value === true) {
- classes.push(base);
- } else if (typeof value === 'string' && value) {
- classes.push(`${base}${value}`);
- }
- });
-
- return classes.join(' ');
-}
-
-export function computeSStyles(
- sStyle: SStyleProp,
- styleDirectiveProps: Record
-): string {
- const styles: string[] = [];
-
- if (sStyle && typeof sStyle === 'object') {
- Object.entries(sStyle).forEach(([key, value]) => {
- if (value) {
- styles.push(`${key}: ${value}`);
- }
- });
- }
-
- Object.entries(styleDirectiveProps).forEach(([key, value]) => {
- const base = key.replace('s-style_', '');
- if (value) {
- styles.push(`${base}: ${value}`);
- }
- });
-
- return styles.join('; ');
-}
diff --git a/packages/lapikit/src/lib/labs/utils/types/index.ts b/packages/lapikit/src/lib/labs/utils/types/index.ts
new file mode 100644
index 0000000..bee8e51
--- /dev/null
+++ b/packages/lapikit/src/lib/labs/utils/types/index.ts
@@ -0,0 +1,16 @@
+import type { SClassProp, SStyleProp } from '$lib/labs/compiler/types/index.js';
+
+export type PropValue = string | boolean | number | null | undefined;
+
+export interface useClassNameProps {
+ baseClass?: string;
+ className?: string;
+ sClass?: SClassProp;
+ classProps?: Record;
+}
+
+export interface useStylesProps {
+ styleAttr?: string;
+ sStyle?: SStyleProp;
+ styleProps?: Record;
+}
diff --git a/packages/lapikit/test-debug.js b/packages/lapikit/test-debug.js
new file mode 100644
index 0000000..a48c01e
--- /dev/null
+++ b/packages/lapikit/test-debug.js
@@ -0,0 +1,7 @@
+import { liliCore } from './src/lib/labs/compiler/preprocess/lili.ts';
+
+const input = '
';
+const result = liliCore().markup({ content: input });
+
+console.log('Result:', result);
+console.log('Code:', result?.code);