@progalaxyelabs/htms-cli 0.3.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/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # HTMS CLI
2
+
3
+ Command-line interface for the HTMS compiler.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @htms/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Compile
14
+
15
+ Compile a `.htms` file to TypeScript:
16
+
17
+ ```bash
18
+ htms compile src/app.htms -o dist/
19
+ ```
20
+
21
+ Options:
22
+ - `-o, --output <dir>` - Output directory (default: `dist`)
23
+ - `-w, --watch` - Watch for changes
24
+ - `-q, --quiet` - Suppress output
25
+
26
+ ### Check
27
+
28
+ Validate a `.htms` file without generating output:
29
+
30
+ ```bash
31
+ htms check src/app.htms
32
+ ```
33
+
34
+ ### Watch Mode
35
+
36
+ Watch for changes and recompile automatically:
37
+
38
+ ```bash
39
+ htms compile src/app.htms -o dist/ --watch
40
+ ```
41
+
42
+ ### Init
43
+
44
+ Initialize a new HTMS project (coming soon):
45
+
46
+ ```bash
47
+ htms init
48
+ ```
49
+
50
+ ## Programmatic API
51
+
52
+ You can also use the CLI programmatically:
53
+
54
+ ```javascript
55
+ import { compileFile, checkFile } from '@htms/cli';
56
+
57
+ const result = await compileFile('src/app.htms', 'dist/');
58
+ console.log(result.success); // true/false
59
+ ```
60
+
61
+ ## Vite Plugin
62
+
63
+ Use HTMS in your Vite project:
64
+
65
+ ```javascript
66
+ // vite.config.js
67
+ import { htmsPlugin } from '@htms/cli/vite';
68
+
69
+ export default {
70
+ plugins: [htmsPlugin({
71
+ include: /\.htms$/,
72
+ outputDir: 'src/generated',
73
+ watch: true
74
+ })]
75
+ };
76
+ ```
77
+
78
+ ### Plugin Options
79
+
80
+ - `include` - RegExp pattern for files to process (default: `/\.htms$/`)
81
+ - `outputDir` - Directory for generated files (default: `'src/generated'`)
82
+ - `watch` - Enable watch mode (default: `true`)
83
+
84
+ ## Output Files
85
+
86
+ The compiler generates three TypeScript files:
87
+
88
+ 1. **templates.ts** - Component/section/page functions
89
+ 2. **router.ts** - Hash-based router with context management
90
+ 3. **events.ts** - Event delegation and two-way binding
91
+
92
+ ## Error Formatting
93
+
94
+ Errors are displayed with source context:
95
+
96
+ ```
97
+ error[E002]: Undefined component: 'NavBar'
98
+ --> src/app.htms:10:5
99
+
100
+ 9 | page home "/" {
101
+ 10 | NavBar
102
+ | ^^^^^^
103
+ 11 | }
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT
package/bin/htms.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { compileFile, checkFile, getStats } from '../src/compiler.js';
5
+ import { watchFiles } from '../src/watch.js';
6
+ import { readFile } from 'fs/promises';
7
+ import { dirname, join } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import pc from 'picocolors';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const packageJson = JSON.parse(
13
+ await readFile(join(__dirname, '../package.json'), 'utf-8')
14
+ );
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('htms')
20
+ .description('HTMS compiler - compile .htms files to TypeScript')
21
+ .version(packageJson.version);
22
+
23
+ // Compile command
24
+ program
25
+ .command('compile')
26
+ .description('Compile .htms file to TypeScript')
27
+ .argument('<input>', 'Input .htms file')
28
+ .option('-o, --output <dir>', 'Output directory', 'dist')
29
+ .option('-w, --watch', 'Watch for changes')
30
+ .option('-q, --quiet', 'Suppress output')
31
+ .action(async (input, options) => {
32
+ try {
33
+ if (options.watch) {
34
+ watchFiles(input, options.output, options);
35
+ } else {
36
+ const result = await compileFile(input, options.output, options);
37
+ const stats = getStats(result);
38
+
39
+ if (result.success) {
40
+ if (!options.quiet) {
41
+ console.log(pc.green('\n✓ Compilation successful!'));
42
+ console.log(pc.gray(` Generated ${stats.files} file(s)`));
43
+ if (stats.warnings > 0) {
44
+ console.log(pc.yellow(` ${stats.warnings} warning(s)`));
45
+ }
46
+ }
47
+ process.exit(0);
48
+ } else {
49
+ if (!options.quiet) {
50
+ console.error(pc.red('\n✗ Compilation failed!'));
51
+ console.error(pc.gray(` ${stats.errors} error(s)`));
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }
56
+ } catch (error) {
57
+ console.error(pc.red('Error:'), error.message);
58
+ process.exit(1);
59
+ }
60
+ });
61
+
62
+ // Check command
63
+ program
64
+ .command('check')
65
+ .description('Check .htms file for errors without generating output')
66
+ .argument('<input>', 'Input .htms file')
67
+ .action(async (input) => {
68
+ try {
69
+ const result = await checkFile(input);
70
+ const stats = getStats(result);
71
+
72
+ if (result.success) {
73
+ console.log(pc.green('\n✓ No errors found!'));
74
+ if (stats.warnings > 0) {
75
+ console.log(pc.yellow(` ${stats.warnings} warning(s)`));
76
+ }
77
+ process.exit(0);
78
+ } else {
79
+ console.error(pc.red('\n✗ Validation failed!'));
80
+ console.error(pc.gray(` ${stats.errors} error(s)`));
81
+ process.exit(1);
82
+ }
83
+ } catch (error) {
84
+ console.error(pc.red('Error:'), error.message);
85
+ process.exit(1);
86
+ }
87
+ });
88
+
89
+ // Init command (scaffold new project)
90
+ program
91
+ .command('init')
92
+ .description('Initialize a new HTMS project')
93
+ .option('-d, --dir <directory>', 'Project directory', '.')
94
+ .action(async (options) => {
95
+ console.log(pc.blue('📦 Initializing new HTMS project...'));
96
+ console.log(pc.gray(` Directory: ${options.dir}\n`));
97
+
98
+ // This would create a basic project structure
99
+ // For now, just show what would be created
100
+ console.log(pc.gray('Would create:'));
101
+ console.log(pc.gray(' src/'));
102
+ console.log(pc.gray(' app.htms'));
103
+ console.log(pc.gray(' actions.ts'));
104
+ console.log(pc.gray(' runtime.ts'));
105
+ console.log(pc.gray(' dist/'));
106
+ console.log(pc.gray(' package.json'));
107
+ console.log(pc.gray(' vite.config.ts\n'));
108
+
109
+ console.log(pc.yellow('⚠ Init command not yet implemented'));
110
+ console.log(pc.gray(' This is a placeholder for future functionality'));
111
+ });
112
+
113
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@progalaxyelabs/htms-cli",
3
+ "version": "0.3.0",
4
+ "description": "CLI for HTMS compiler",
5
+ "type": "module",
6
+ "bin": {
7
+ "htms": "./bin/htms.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js",
12
+ "./vite": "./src/vite-plugin.js"
13
+ },
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "keywords": ["htms", "compiler", "cli"],
18
+ "author": "ProGalaxy Labs",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@progalaxyelabs/htms-compiler": "^0.3.0",
22
+ "commander": "^11.0.0",
23
+ "chokidar": "^3.5.0",
24
+ "picocolors": "^1.0.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }
@@ -0,0 +1,87 @@
1
+ import { compile_wasm, init } from '@progalaxyelabs/htms-compiler';
2
+ import { readFile, writeFile, mkdir } from 'fs/promises';
3
+ import { dirname, join, basename } from 'path';
4
+ import { printDiagnostics } from './format-errors.js';
5
+ import pc from 'picocolors';
6
+
7
+ // Initialize WASM module
8
+ let wasmInitialized = false;
9
+ async function ensureWasmInit() {
10
+ if (!wasmInitialized) {
11
+ await init();
12
+ wasmInitialized = true;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Compile HTMS file to TypeScript
18
+ * @param {string} inputPath - Path to .htms file
19
+ * @param {string} outputDir - Output directory for generated files
20
+ * @param {object} options - Compilation options
21
+ * @returns {Promise<{success: boolean, diagnostics: Array}>}
22
+ */
23
+ export async function compileFile(inputPath, outputDir, options = {}) {
24
+ await ensureWasmInit();
25
+
26
+ // Read source file
27
+ const source = await readFile(inputPath, 'utf-8');
28
+
29
+ // Compile using WASM
30
+ const result = compile_wasm(source);
31
+
32
+ // Print diagnostics
33
+ if (result.diagnostics && result.diagnostics.length > 0) {
34
+ printDiagnostics(result.diagnostics, source, inputPath);
35
+ }
36
+
37
+ // Write output files if successful
38
+ if (result.success && result.files) {
39
+ await mkdir(outputDir, { recursive: true });
40
+
41
+ for (const file of result.files) {
42
+ const outputPath = join(outputDir, file.path);
43
+ await writeFile(outputPath, file.content, 'utf-8');
44
+
45
+ if (!options.quiet) {
46
+ console.log(pc.green('✓') + ` Generated ${pc.cyan(file.path)}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * Check HTMS file for errors without generating output
56
+ * @param {string} inputPath - Path to .htms file
57
+ * @returns {Promise<{success: boolean, diagnostics: Array}>}
58
+ */
59
+ export async function checkFile(inputPath) {
60
+ await ensureWasmInit();
61
+
62
+ // Read source file
63
+ const source = await readFile(inputPath, 'utf-8');
64
+
65
+ // Compile (but don't write output)
66
+ const result = compile_wasm(source);
67
+
68
+ // Print diagnostics
69
+ if (result.diagnostics && result.diagnostics.length > 0) {
70
+ printDiagnostics(result.diagnostics, source, inputPath);
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Get compilation statistics
78
+ * @param {object} result - Compilation result
79
+ * @returns {object} Statistics
80
+ */
81
+ export function getStats(result) {
82
+ const errors = result.diagnostics?.filter(d => d.severity === 'Error').length || 0;
83
+ const warnings = result.diagnostics?.filter(d => d.severity === 'Warning').length || 0;
84
+ const files = result.files?.length || 0;
85
+
86
+ return { errors, warnings, files };
87
+ }
@@ -0,0 +1,73 @@
1
+ import pc from 'picocolors';
2
+
3
+ /**
4
+ * Format diagnostics with source context
5
+ * @param {Array} diagnostics - Array of diagnostic objects
6
+ * @param {string} source - Original source code
7
+ * @param {string} filename - Filename for display
8
+ * @returns {string} Formatted error messages
9
+ */
10
+ export function formatDiagnostics(diagnostics, source, filename = 'input.htms') {
11
+ if (diagnostics.length === 0) {
12
+ return '';
13
+ }
14
+
15
+ const lines = source.split('\n');
16
+ const output = [];
17
+
18
+ for (const diag of diagnostics) {
19
+ const { severity, message, location, code } = diag;
20
+ const { line, column } = location;
21
+
22
+ // Header with severity
23
+ const severityStr = severity === 'Error'
24
+ ? pc.red(pc.bold('error'))
25
+ : pc.yellow(pc.bold('warning'));
26
+
27
+ const codeStr = code ? pc.gray(`[${code}]`) : '';
28
+ output.push(`${severityStr}${codeStr}: ${message}`);
29
+
30
+ // Location
31
+ output.push(pc.cyan(` --> ${filename}:${line}:${column}`));
32
+ output.push('');
33
+
34
+ // Source context (line before, error line, line after)
35
+ const startLine = Math.max(1, line - 1);
36
+ const endLine = Math.min(lines.length, line + 1);
37
+
38
+ for (let i = startLine; i <= endLine; i++) {
39
+ const lineNum = String(i).padStart(4, ' ');
40
+ const sourceLine = lines[i - 1] || '';
41
+
42
+ if (i === line) {
43
+ // Error line
44
+ output.push(pc.gray(`${lineNum} | `) + sourceLine);
45
+
46
+ // Underline the error
47
+ const padding = ' '.repeat(column - 1);
48
+ const underline = pc.red('^'.repeat(Math.max(1, sourceLine.length - column + 1)));
49
+ output.push(pc.gray(' | ') + padding + underline);
50
+ } else {
51
+ // Context line
52
+ output.push(pc.gray(`${lineNum} | ${sourceLine}`));
53
+ }
54
+ }
55
+
56
+ output.push('');
57
+ }
58
+
59
+ return output.join('\n');
60
+ }
61
+
62
+ /**
63
+ * Print diagnostics to console
64
+ * @param {Array} diagnostics - Array of diagnostic objects
65
+ * @param {string} source - Original source code
66
+ * @param {string} filename - Filename for display
67
+ */
68
+ export function printDiagnostics(diagnostics, source, filename) {
69
+ const formatted = formatDiagnostics(diagnostics, source, filename);
70
+ if (formatted) {
71
+ console.error(formatted);
72
+ }
73
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * HTMS CLI - Main exports
3
+ */
4
+
5
+ export { compileFile, checkFile, getStats } from './compiler.js';
6
+ export { watchFiles } from './watch.js';
7
+ export { formatDiagnostics, printDiagnostics } from './format-errors.js';
8
+ export { htmsPlugin } from './vite-plugin.js';
@@ -0,0 +1,133 @@
1
+ import { compile_wasm, init } from '@htms/compiler';
2
+ import { readFile, writeFile } from 'fs/promises';
3
+ import { dirname, join, relative } from 'path';
4
+ import pc from 'picocolors';
5
+
6
+ let wasmInitialized = false;
7
+
8
+ /**
9
+ * Vite plugin for HTMS compilation
10
+ * @param {object} options - Plugin options
11
+ * @returns {import('vite').Plugin}
12
+ */
13
+ export function htmsPlugin(options = {}) {
14
+ const {
15
+ include = /\.htms$/,
16
+ outputDir = 'src/generated',
17
+ watch = true
18
+ } = options;
19
+
20
+ return {
21
+ name: 'vite-plugin-htms',
22
+
23
+ async buildStart() {
24
+ if (!wasmInitialized) {
25
+ await init();
26
+ wasmInitialized = true;
27
+ }
28
+ },
29
+
30
+ async handleHotUpdate({ file, server }) {
31
+ // Only process .htms files
32
+ if (!include.test(file)) {
33
+ return;
34
+ }
35
+
36
+ console.log(pc.blue('[htms]') + ` Compiling ${pc.cyan(relative(process.cwd(), file))}`);
37
+
38
+ try {
39
+ const source = await readFile(file, 'utf-8');
40
+ const result = compile_wasm(source);
41
+
42
+ if (result.success && result.files) {
43
+ // Write generated files
44
+ for (const genFile of result.files) {
45
+ const outputPath = join(outputDir, genFile.path);
46
+ await writeFile(outputPath, genFile.content, 'utf-8');
47
+ console.log(pc.green(' ✓') + ` Generated ${pc.gray(genFile.path)}`);
48
+ }
49
+
50
+ // Trigger HMR for generated files
51
+ const modules = [];
52
+ for (const genFile of result.files) {
53
+ const modulePath = join(outputDir, genFile.path);
54
+ const module = server.moduleGraph.getModuleById(modulePath);
55
+ if (module) {
56
+ modules.push(module);
57
+ }
58
+ }
59
+
60
+ if (modules.length > 0) {
61
+ return modules;
62
+ }
63
+ } else {
64
+ // Print errors
65
+ console.error(pc.red(' ✗') + ` Compilation failed`);
66
+ for (const diag of result.diagnostics || []) {
67
+ if (diag.severity === 'Error') {
68
+ console.error(pc.red(` ${diag.message}`) + pc.gray(` at line ${diag.location.line}`));
69
+ }
70
+ }
71
+ }
72
+ } catch (error) {
73
+ console.error(pc.red('[htms]') + ` Error: ${error.message}`);
74
+ }
75
+
76
+ return [];
77
+ },
78
+
79
+ async transform(code, id) {
80
+ // Only process .htms files
81
+ if (!include.test(id)) {
82
+ return null;
83
+ }
84
+
85
+ try {
86
+ const result = compile_wasm(code);
87
+
88
+ if (!result.success) {
89
+ // Show errors in console
90
+ console.error(pc.red('[htms]') + ` Compilation failed for ${id}`);
91
+ for (const diag of result.diagnostics || []) {
92
+ if (diag.severity === 'Error') {
93
+ console.error(pc.red(` ${diag.message}`) + pc.gray(` at line ${diag.location.line}`));
94
+ }
95
+ }
96
+
97
+ // Return error as module
98
+ const errorMsg = result.diagnostics
99
+ ?.filter(d => d.severity === 'Error')
100
+ .map(d => `${d.message} (line ${d.location.line})`)
101
+ .join('\\n');
102
+
103
+ return {
104
+ code: `throw new Error('HTMS compilation failed:\\n${errorMsg}');`,
105
+ map: null
106
+ };
107
+ }
108
+
109
+ // Write generated files to output directory
110
+ if (result.files) {
111
+ for (const file of result.files) {
112
+ const outputPath = join(outputDir, file.path);
113
+ await writeFile(outputPath, file.content, 'utf-8');
114
+ }
115
+ }
116
+
117
+ // Return a module that imports the generated templates
118
+ return {
119
+ code: `export { default } from '${outputDir}/templates.js';`,
120
+ map: null
121
+ };
122
+ } catch (error) {
123
+ console.error(pc.red('[htms]') + ` Error processing ${id}:`, error.message);
124
+ return {
125
+ code: `throw new Error('HTMS plugin error: ${error.message}');`,
126
+ map: null
127
+ };
128
+ }
129
+ }
130
+ };
131
+ }
132
+
133
+ export default htmsPlugin;
package/src/watch.js ADDED
@@ -0,0 +1,60 @@
1
+ import chokidar from 'chokidar';
2
+ import { compileFile, getStats } from './compiler.js';
3
+ import pc from 'picocolors';
4
+
5
+ /**
6
+ * Watch HTMS files for changes and recompile
7
+ * @param {string} pattern - Glob pattern for files to watch
8
+ * @param {string} outputDir - Output directory
9
+ * @param {object} options - Watch options
10
+ */
11
+ export function watchFiles(pattern, outputDir, options = {}) {
12
+ console.log(pc.blue('👁 Watching for changes...'));
13
+ console.log(pc.gray(` Pattern: ${pattern}`));
14
+ console.log(pc.gray(` Output: ${outputDir}\n`));
15
+
16
+ const watcher = chokidar.watch(pattern, {
17
+ persistent: true,
18
+ ignoreInitial: false,
19
+ awaitWriteFinish: {
20
+ stabilityThreshold: 100,
21
+ pollInterval: 50
22
+ }
23
+ });
24
+
25
+ watcher
26
+ .on('add', path => handleChange(path, outputDir, 'added', options))
27
+ .on('change', path => handleChange(path, outputDir, 'changed', options))
28
+ .on('error', error => console.error(pc.red(`Watcher error: ${error}`)));
29
+
30
+ // Handle graceful shutdown
31
+ process.on('SIGINT', () => {
32
+ console.log('\n' + pc.blue('Stopping watcher...'));
33
+ watcher.close();
34
+ process.exit(0);
35
+ });
36
+
37
+ return watcher;
38
+ }
39
+
40
+ async function handleChange(filePath, outputDir, action, options) {
41
+ const timestamp = new Date().toLocaleTimeString();
42
+ console.log(pc.gray(`[${timestamp}]`) + ` File ${action}: ${pc.cyan(filePath)}`);
43
+
44
+ try {
45
+ const result = await compileFile(filePath, outputDir, { ...options, quiet: false });
46
+ const stats = getStats(result);
47
+
48
+ if (result.success) {
49
+ console.log(pc.green('✓') + ` Compiled successfully (${stats.files} files generated)`);
50
+ if (stats.warnings > 0) {
51
+ console.log(pc.yellow('⚠') + ` ${stats.warnings} warning(s)`);
52
+ }
53
+ } else {
54
+ console.log(pc.red('✗') + ` Compilation failed (${stats.errors} error(s))`);
55
+ }
56
+ console.log('');
57
+ } catch (error) {
58
+ console.error(pc.red('Error:') + ` ${error.message}\n`);
59
+ }
60
+ }
@@ -0,0 +1,3 @@
1
+ page home "/" {
2
+ UndefinedComponent
3
+ }
@@ -0,0 +1,108 @@
1
+ // Generated by HTMS Compiler
2
+ // Do not edit manually
3
+
4
+ import { getContext, setContext, rerender } from './router';
5
+ import { actions } from './actions';
6
+
7
+ export interface ActionContext {
8
+ data: Record<string, unknown>;
9
+ rerender: () => void;
10
+ }
11
+
12
+ export function initEvents(): void {
13
+ // Event delegation
14
+ document.addEventListener('click', handleEvent);
15
+ document.addEventListener('submit', handleEvent);
16
+ document.addEventListener('input', handleEvent);
17
+ document.addEventListener('change', handleEvent);
18
+ document.addEventListener('blur', handleEvent, true);
19
+ document.addEventListener('focus', handleEvent, true);
20
+
21
+ // Two-way binding
22
+ initBinding();
23
+ }
24
+
25
+ function handleEvent(event: Event): void {
26
+ const target = event.target as HTMLElement;
27
+ const actionEl = target.closest('[data-action]') as HTMLElement;
28
+
29
+ if (!actionEl) return;
30
+
31
+ const actionName = actionEl.dataset.action;
32
+ const eventType = actionEl.dataset.event;
33
+
34
+ // Only handle if event type matches
35
+ if (eventType && eventType !== event.type) return;
36
+
37
+ // Handle modifiers
38
+ if (actionEl.dataset.prevent === 'true') {
39
+ event.preventDefault();
40
+ }
41
+ if (actionEl.dataset.stop === 'true') {
42
+ event.stopPropagation();
43
+ }
44
+
45
+ // Get action
46
+ const action = (actions as Record<string, Function>)[actionName!];
47
+ if (action) {
48
+ const ctx: ActionContext = {
49
+ data: getContext(),
50
+ rerender: () => {
51
+ setContext(ctx.data);
52
+ rerender();
53
+ }
54
+ };
55
+
56
+ // Parse arguments if present
57
+ const argsStr = actionEl.dataset.args;
58
+ if (argsStr) {
59
+ try {
60
+ const args = JSON.parse(argsStr);
61
+ action(...args)(ctx, event);
62
+ } catch {
63
+ action(ctx, event);
64
+ }
65
+ } else {
66
+ action(ctx, event);
67
+ }
68
+
69
+ // Handle once modifier
70
+ if (actionEl.dataset.once === 'true') {
71
+ actionEl.removeAttribute('data-action');
72
+ }
73
+ }
74
+ }
75
+
76
+ function initBinding(): void {
77
+ document.addEventListener('input', (e) => {
78
+ const el = e.target as HTMLInputElement;
79
+ const bindPath = el.dataset.bind;
80
+ if (bindPath) {
81
+ const ctx = getContext();
82
+ setNestedValue(ctx, bindPath, el.value);
83
+ setContext(ctx);
84
+ }
85
+ });
86
+
87
+ document.addEventListener('change', (e) => {
88
+ const el = e.target as HTMLInputElement;
89
+ const bindPath = el.dataset.bind;
90
+ if (bindPath && el.type === 'checkbox') {
91
+ const ctx = getContext();
92
+ setNestedValue(ctx, bindPath, el.checked);
93
+ setContext(ctx);
94
+ }
95
+ });
96
+ }
97
+
98
+ function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
99
+ const keys = path.split('.');
100
+ const last = keys.pop()!;
101
+ const target = keys.reduce((o, k) => {
102
+ if (!(o as Record<string, unknown>)[k]) {
103
+ (o as Record<string, unknown>)[k] = {};
104
+ }
105
+ return (o as Record<string, unknown>)[k];
106
+ }, obj) as Record<string, unknown>;
107
+ target[last] = value;
108
+ }
@@ -0,0 +1,58 @@
1
+ // Generated by HTMS Compiler
2
+ // Do not edit manually
3
+
4
+ import { Router } from './runtime';
5
+ import { HomePage, AboutPage } from './templates';
6
+
7
+ // Application context
8
+ let context: Record<string, unknown> = {};
9
+ let currentPage: string = '';
10
+ const appContainer = document.getElementById('app');
11
+
12
+ export function getContext(): Record<string, unknown> {
13
+ return context;
14
+ }
15
+
16
+ export function setContext(data: Record<string, unknown>): void {
17
+ context = data;
18
+ }
19
+
20
+ export function rerender(): void {
21
+ if (currentPage && appContainer) {
22
+ const renderer = routes[currentPage];
23
+ if (renderer) {
24
+ appContainer.innerHTML = '';
25
+ appContainer.appendChild(renderer(context));
26
+ }
27
+ }
28
+ }
29
+
30
+ // Route definitions
31
+ const routes: Record<string, (ctx: Record<string, unknown>) => HTMLElement> = {
32
+ '/': HomePage,
33
+ '/about': AboutPage,
34
+ };
35
+
36
+ function renderPage(route: string): void {
37
+ currentPage = route;
38
+ const renderer = routes[route];
39
+ if (renderer && appContainer) {
40
+ appContainer.innerHTML = '';
41
+ appContainer.appendChild(renderer(context));
42
+ } else if (appContainer) {
43
+ const el = document.createElement('h1');
44
+ el.textContent = '404 - Page Not Found';
45
+ appContainer.innerHTML = '';
46
+ appContainer.appendChild(el);
47
+ }
48
+ }
49
+
50
+ // Create router
51
+ export const router = new Router({
52
+ mode: 'hash',
53
+ routes: {
54
+ '/': () => renderPage('/'),
55
+ '/about': () => renderPage('/about'),
56
+ },
57
+ notFound: () => renderPage('__404__'),
58
+ });
@@ -0,0 +1,52 @@
1
+ // Generated by HTMS Compiler
2
+ // Do not edit manually
3
+
4
+ import { actions } from './actions';
5
+
6
+ export type Context = Record<string, unknown>;
7
+
8
+ export function NavBar(ctx: Context): HTMLElement {
9
+ const el0 = document.createElement('nav');
10
+ el0.className = 'navbar';
11
+ const el1 = document.createElement('a');
12
+ el1.href = '#/';
13
+ const el2 = document.createTextNode('Home');
14
+ el1.appendChild(el2);
15
+ el0.appendChild(el1);
16
+ const el3 = document.createElement('a');
17
+ el3.href = '#/about';
18
+ const el4 = document.createTextNode('About');
19
+ el3.appendChild(el4);
20
+ el0.appendChild(el3);
21
+ return el0;
22
+ }
23
+
24
+ export function HomePage(ctx: Context): HTMLElement {
25
+ const fragment = document.createDocumentFragment();
26
+ const el0 = NavBar(ctx);
27
+ fragment.appendChild(el0);
28
+ const el1 = document.createElement('div');
29
+ el1.className = 'hero';
30
+ const el2 = document.createElement('h1');
31
+ const el3 = document.createTextNode('Welcome to HTMS!');
32
+ el2.appendChild(el3);
33
+ el1.appendChild(el2);
34
+ fragment.appendChild(el1);
35
+ const root = document.createElement('div');
36
+ root.appendChild(fragment);
37
+ return root;
38
+ }
39
+
40
+ export function AboutPage(ctx: Context): HTMLElement {
41
+ const fragment = document.createDocumentFragment();
42
+ const el0 = NavBar(ctx);
43
+ fragment.appendChild(el0);
44
+ const el1 = document.createElement('div');
45
+ const el2 = document.createTextNode('About Page');
46
+ el1.appendChild(el2);
47
+ fragment.appendChild(el1);
48
+ const root = document.createElement('div');
49
+ root.appendChild(fragment);
50
+ return root;
51
+ }
52
+
package/test/test.htms ADDED
@@ -0,0 +1,18 @@
1
+ component NavBar {
2
+ nav [class: "navbar"] {
3
+ a [href: "#/"] {{ Home }}
4
+ a [href: "#/about"] {{ About }}
5
+ }
6
+ }
7
+
8
+ page home "/" {
9
+ NavBar
10
+ div [class: "hero"] {
11
+ h1 {{ Welcome to HTMS! }}
12
+ }
13
+ }
14
+
15
+ page about "/about" {
16
+ NavBar
17
+ div {{ About Page }}
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "allowJs": true,
8
+ "checkJs": false,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*.js", "bin/**/*.js"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }