primo-cli 0.1.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 +183 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +379 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +261 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +516 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +163 -0
- package/dist/commands/import.d.ts +9 -0
- package/dist/commands/import.js +118 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +68 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/new.d.ts +7 -0
- package/dist/commands/new.js +507 -0
- package/dist/commands/publish.d.ts +6 -0
- package/dist/commands/publish.js +239 -0
- package/dist/commands/pull.d.ts +8 -0
- package/dist/commands/pull.js +243 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +118 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +514 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +70 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +29 -0
- package/dist/utils/binary.d.ts +5 -0
- package/dist/utils/binary.js +129 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Primo CLI
|
|
2
|
+
|
|
3
|
+
Local development CLI for [Primo](https://primocms.org) - build and edit sites with a visual CMS.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g primo-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Create a new site
|
|
15
|
+
primo new my-site
|
|
16
|
+
|
|
17
|
+
# This starts the local CMS automatically
|
|
18
|
+
# Edit at: http://my-site.localhost:3000/admin/site
|
|
19
|
+
# Preview at: http://my-site.localhost:3000/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### `primo new [name]`
|
|
25
|
+
|
|
26
|
+
Create a new site with starter files.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
primo new # Interactive prompt for name
|
|
30
|
+
primo new my-site # Create "my-site" directory
|
|
31
|
+
primo new --skip-dev # Create files without starting CMS
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `primo dev`
|
|
35
|
+
|
|
36
|
+
Start the local CMS server. Watches for file changes and syncs edits from the CMS back to local files.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
primo dev # Start in current directory
|
|
40
|
+
primo dev -p 8080 # Use custom port
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `primo push`
|
|
44
|
+
|
|
45
|
+
Push local files to a hosted Primo instance.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
primo push -s https://cms.example.com --site abc123
|
|
49
|
+
primo push --preview # Preview changes without applying
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
- `-s, --server <url>` - Server URL
|
|
54
|
+
- `--site <id>` - Site ID
|
|
55
|
+
- `-d, --dir <dir>` - Directory (default: `.`)
|
|
56
|
+
- `-t, --token <token>` - Auth token
|
|
57
|
+
- `--preview` - Preview only
|
|
58
|
+
|
|
59
|
+
### `primo pull`
|
|
60
|
+
|
|
61
|
+
Pull from a hosted Primo instance to local files.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
primo pull -s https://cms.example.com
|
|
65
|
+
primo pull --site abc123 -o ./my-site
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
- `-s, --server <url>` - Server URL (auto-detects local)
|
|
70
|
+
- `--site <id>` - Site ID (interactive if not provided)
|
|
71
|
+
- `-o, --output <dir>` - Output directory (default: `.`)
|
|
72
|
+
- `-t, --token <token>` - Auth token
|
|
73
|
+
|
|
74
|
+
### `primo login`
|
|
75
|
+
|
|
76
|
+
Authenticate with a hosted Primo instance.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
primo login https://cms.example.com
|
|
80
|
+
primo login https://cms.example.com -e user@example.com
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `primo publish`
|
|
84
|
+
|
|
85
|
+
Deploy your site with CMS to Railway or Fly.io.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
primo publish # Interactive provider selection
|
|
89
|
+
primo publish -p railway # Deploy to Railway
|
|
90
|
+
primo publish -p fly # Deploy to Fly.io
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `primo validate`
|
|
94
|
+
|
|
95
|
+
Check site structure for errors.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
primo validate
|
|
99
|
+
primo validate --strict # Strict mode
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `primo build`
|
|
103
|
+
|
|
104
|
+
Build static HTML site for deployment to any static host.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
primo build # Output to ./dist
|
|
108
|
+
primo build -o ./public # Custom output directory
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Deploy the output anywhere:
|
|
112
|
+
```bash
|
|
113
|
+
# Netlify
|
|
114
|
+
npx netlify deploy --prod --dir=dist
|
|
115
|
+
|
|
116
|
+
# Vercel
|
|
117
|
+
npx vercel dist
|
|
118
|
+
|
|
119
|
+
# Cloudflare Pages
|
|
120
|
+
npx wrangler pages deploy dist
|
|
121
|
+
|
|
122
|
+
# Or just push to a repo connected to any static host
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Site Structure
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
my-site/
|
|
129
|
+
├── primo.json # Site config (name, site_id, host)
|
|
130
|
+
├── blocks/ # Svelte components
|
|
131
|
+
│ └── hero/
|
|
132
|
+
│ ├── component.svelte
|
|
133
|
+
│ ├── fields.json
|
|
134
|
+
│ └── content.yaml
|
|
135
|
+
├── pages/ # Page content (YAML)
|
|
136
|
+
│ └── index.yaml
|
|
137
|
+
├── page-types/ # Page templates
|
|
138
|
+
│ └── default/
|
|
139
|
+
│ └── config.json
|
|
140
|
+
├── site/ # Site-wide settings
|
|
141
|
+
│ ├── fields.json
|
|
142
|
+
│ ├── content.yaml
|
|
143
|
+
│ └── head.svelte
|
|
144
|
+
└── uploads/ # Media files
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Multi-Site Mode
|
|
148
|
+
|
|
149
|
+
Manage multiple sites from one directory:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
workspace/
|
|
153
|
+
├── server.json
|
|
154
|
+
├── site-one/
|
|
155
|
+
│ ├── primo.json
|
|
156
|
+
│ ├── blocks/
|
|
157
|
+
│ └── pages/
|
|
158
|
+
└── site-two/
|
|
159
|
+
├── primo.json
|
|
160
|
+
├── blocks/
|
|
161
|
+
└── pages/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
// server.json
|
|
166
|
+
{ "port": 3000 }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
cd workspace
|
|
171
|
+
primo dev
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Each site gets its own subdomain: `site-one.localhost:3000`, `site-two.localhost:3000`
|
|
175
|
+
|
|
176
|
+
## Documentation
|
|
177
|
+
|
|
178
|
+
Full documentation: [docs.primocms.org](https://docs.primocms.org)
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- Node.js 18+
|
|
183
|
+
- For `primo publish`: Railway CLI or Fly.io CLI
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { load as load_yaml } from 'js-yaml';
|
|
6
|
+
export async function build_site(options) {
|
|
7
|
+
const spinner = ora('Building site...').start();
|
|
8
|
+
try {
|
|
9
|
+
const site_dir = path.resolve(options.dir);
|
|
10
|
+
const output_dir = path.resolve(options.output);
|
|
11
|
+
// Read site config
|
|
12
|
+
const config_path = path.join(site_dir, 'primo.json');
|
|
13
|
+
let config;
|
|
14
|
+
try {
|
|
15
|
+
const config_data = await fs.readFile(config_path, 'utf-8');
|
|
16
|
+
config = JSON.parse(config_data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
spinner.fail('No primo.json found. Run `primo new` first.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Clean and create output directory
|
|
23
|
+
await fs.rm(output_dir, { recursive: true, force: true });
|
|
24
|
+
await fs.mkdir(output_dir, { recursive: true });
|
|
25
|
+
// Read head.svelte for global styles
|
|
26
|
+
let head_content = '';
|
|
27
|
+
try {
|
|
28
|
+
const head_path = path.join(site_dir, 'site', 'head.svelte');
|
|
29
|
+
head_content = await fs.readFile(head_path, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No head.svelte, that's fine
|
|
33
|
+
}
|
|
34
|
+
// Extract CSS from head.svelte
|
|
35
|
+
let global_css = '';
|
|
36
|
+
if (head_content) {
|
|
37
|
+
const style_match = head_content.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
38
|
+
if (style_match) {
|
|
39
|
+
global_css = style_match[1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Find all pages
|
|
43
|
+
const pages_dir = path.join(site_dir, 'pages');
|
|
44
|
+
const pages = await find_pages(pages_dir);
|
|
45
|
+
spinner.text = `Building ${pages.length} page${pages.length !== 1 ? 's' : ''}...`;
|
|
46
|
+
// Collect all CSS from blocks
|
|
47
|
+
const block_css_cache = new Map();
|
|
48
|
+
const all_css = [global_css];
|
|
49
|
+
// Build each page
|
|
50
|
+
for (const page_file of pages) {
|
|
51
|
+
const page_content = await fs.readFile(page_file, 'utf-8');
|
|
52
|
+
const page = load_yaml(page_content);
|
|
53
|
+
// Render page sections
|
|
54
|
+
let page_html = '';
|
|
55
|
+
for (const section of page.sections || []) {
|
|
56
|
+
const block_name = section.block;
|
|
57
|
+
const content = normalize_content(section.content);
|
|
58
|
+
// Get or compile block
|
|
59
|
+
let block_css = block_css_cache.get(block_name);
|
|
60
|
+
if (block_css === undefined) {
|
|
61
|
+
const result = await compile_block(site_dir, block_name);
|
|
62
|
+
block_css = result.css;
|
|
63
|
+
block_css_cache.set(block_name, block_css);
|
|
64
|
+
if (block_css) {
|
|
65
|
+
all_css.push(block_css);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Render block HTML
|
|
69
|
+
const block_html = await render_block(site_dir, block_name, content);
|
|
70
|
+
page_html += block_html;
|
|
71
|
+
}
|
|
72
|
+
// Generate full HTML document
|
|
73
|
+
const html = generate_html(config.name, page.name, page_html, all_css.join('\n'));
|
|
74
|
+
// Determine output path
|
|
75
|
+
const slug = page.slug || path.basename(page_file, '.yaml');
|
|
76
|
+
const out_path = slug === '' || slug === 'index'
|
|
77
|
+
? path.join(output_dir, 'index.html')
|
|
78
|
+
: path.join(output_dir, slug, 'index.html');
|
|
79
|
+
await fs.mkdir(path.dirname(out_path), { recursive: true });
|
|
80
|
+
await fs.writeFile(out_path, html);
|
|
81
|
+
}
|
|
82
|
+
// Copy uploads directory
|
|
83
|
+
const uploads_src = path.join(site_dir, 'uploads');
|
|
84
|
+
const uploads_dest = path.join(output_dir, 'uploads');
|
|
85
|
+
try {
|
|
86
|
+
await copy_dir(uploads_src, uploads_dest);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No uploads directory, that's fine
|
|
90
|
+
}
|
|
91
|
+
spinner.succeed(`Built ${pages.length} page${pages.length !== 1 ? 's' : ''} to ${chalk.cyan(output_dir)}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.dim(' Preview locally:'));
|
|
94
|
+
console.log(chalk.dim(` npx serve ${options.output}`));
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(chalk.dim(' Deploy:'));
|
|
97
|
+
console.log(chalk.dim(' npx netlify deploy --prod --dir=' + options.output));
|
|
98
|
+
console.log(chalk.dim(' npx vercel ' + options.output));
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
spinner.fail(`Build failed: ${error instanceof Error ? error.message : error}`);
|
|
103
|
+
if (error instanceof Error && error.stack) {
|
|
104
|
+
console.log(chalk.dim(error.stack));
|
|
105
|
+
}
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function find_pages(pages_dir) {
|
|
110
|
+
const pages = [];
|
|
111
|
+
async function scan(dir) {
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const full_path = path.join(dir, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
await scan(full_path);
|
|
123
|
+
}
|
|
124
|
+
else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
125
|
+
pages.push(full_path);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
await scan(pages_dir);
|
|
130
|
+
return pages;
|
|
131
|
+
}
|
|
132
|
+
async function compile_block(site_dir, block_name) {
|
|
133
|
+
const component_path = path.join(site_dir, 'blocks', block_name, 'component.svelte');
|
|
134
|
+
try {
|
|
135
|
+
const source = await fs.readFile(component_path, 'utf-8');
|
|
136
|
+
// Extract CSS from the component
|
|
137
|
+
const style_match = source.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
|
|
138
|
+
const css = style_match ? style_match[1] : '';
|
|
139
|
+
return { css };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { css: '' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function render_block(site_dir, block_name, content) {
|
|
146
|
+
const component_path = path.join(site_dir, 'blocks', block_name, 'component.svelte');
|
|
147
|
+
try {
|
|
148
|
+
let source = await fs.readFile(component_path, 'utf-8');
|
|
149
|
+
// Remove script tags for static render
|
|
150
|
+
source = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
151
|
+
// Remove style tags (CSS is collected separately)
|
|
152
|
+
source = source.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
153
|
+
// Simple template rendering - replace {variable} with content values
|
|
154
|
+
let html = source.trim();
|
|
155
|
+
// Handle {#if condition}...{/if} blocks
|
|
156
|
+
html = process_if_blocks(html, content);
|
|
157
|
+
// Handle {#each items as item}...{/each} blocks
|
|
158
|
+
html = process_each_blocks(html, content);
|
|
159
|
+
// Replace simple variable interpolations {variable}
|
|
160
|
+
html = html.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (match, varName) => {
|
|
161
|
+
const value = content[varName];
|
|
162
|
+
if (value === undefined || value === null)
|
|
163
|
+
return '';
|
|
164
|
+
return escape_html(String(value));
|
|
165
|
+
});
|
|
166
|
+
// Replace property access {obj.prop} or {obj?.prop}
|
|
167
|
+
html = html.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\??\.([\w.?]+)\}/g, (match, objName, propPath) => {
|
|
168
|
+
const obj = content[objName];
|
|
169
|
+
if (!obj || typeof obj !== 'object')
|
|
170
|
+
return '';
|
|
171
|
+
const value = get_nested_value(obj, propPath.replace(/\?/g, ''));
|
|
172
|
+
if (value === undefined || value === null)
|
|
173
|
+
return '';
|
|
174
|
+
return escape_html(String(value));
|
|
175
|
+
});
|
|
176
|
+
// Replace {obj && obj.prop} patterns
|
|
177
|
+
html = html.replace(/\{[^}]+&&\s*([^}]+)\}/g, (match, expr) => {
|
|
178
|
+
// Try to evaluate simple property access
|
|
179
|
+
const prop_match = expr.trim().match(/([a-zA-Z_][a-zA-Z0-9_]*)\.(\w+)/);
|
|
180
|
+
if (prop_match) {
|
|
181
|
+
const obj = content[prop_match[1]];
|
|
182
|
+
if (obj && typeof obj === 'object') {
|
|
183
|
+
const value = obj[prop_match[2]];
|
|
184
|
+
if (value !== undefined && value !== null) {
|
|
185
|
+
return escape_html(String(value));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
});
|
|
191
|
+
// Replace (obj && obj.prop) || 'default' patterns
|
|
192
|
+
html = html.replace(/\{[^}]*\|\|\s*['"]([^'"]+)['"]\}/g, (match, defaultVal) => {
|
|
193
|
+
// Check if the first part evaluates to something
|
|
194
|
+
const var_match = match.match(/\{([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
195
|
+
if (var_match) {
|
|
196
|
+
const value = content[var_match[1]];
|
|
197
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
198
|
+
return escape_html(String(value));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return escape_html(defaultVal);
|
|
202
|
+
});
|
|
203
|
+
// Handle {@html content} - render raw HTML
|
|
204
|
+
html = html.replace(/\{@html\s+([^}]+)\}/g, (match, expr) => {
|
|
205
|
+
const var_name = expr.trim();
|
|
206
|
+
const value = content[var_name];
|
|
207
|
+
if (value === undefined || value === null)
|
|
208
|
+
return '';
|
|
209
|
+
return String(value); // Don't escape - it's raw HTML
|
|
210
|
+
});
|
|
211
|
+
// Clean up any remaining unresolved template expressions
|
|
212
|
+
html = html.replace(/\{[^}]+\}/g, '');
|
|
213
|
+
return html;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.log(chalk.yellow(` Warning: Could not render block "${block_name}": ${error}`));
|
|
217
|
+
return `<!-- Block "${block_name}" could not be rendered -->`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function process_if_blocks(html, content) {
|
|
221
|
+
// Handle {#if condition}...{:else}...{/if} and {#if condition}...{/if}
|
|
222
|
+
const if_regex = /\{#if\s+([^}]+)\}([\s\S]*?)\{\/if\}/g;
|
|
223
|
+
return html.replace(if_regex, (match, condition, inner) => {
|
|
224
|
+
// Split by {:else} if present
|
|
225
|
+
const parts = inner.split(/\{:else\}/);
|
|
226
|
+
const if_content = parts[0];
|
|
227
|
+
const else_content = parts[1] || '';
|
|
228
|
+
// Evaluate condition
|
|
229
|
+
const is_truthy = evaluate_condition(condition.trim(), content);
|
|
230
|
+
return is_truthy ? if_content : else_content;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function evaluate_condition(condition, content) {
|
|
234
|
+
// Handle obj?.prop patterns
|
|
235
|
+
const optional_match = condition.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\?\.(\w+)$/);
|
|
236
|
+
if (optional_match) {
|
|
237
|
+
const obj = content[optional_match[1]];
|
|
238
|
+
if (!obj || typeof obj !== 'object')
|
|
239
|
+
return false;
|
|
240
|
+
return !!obj[optional_match[2]];
|
|
241
|
+
}
|
|
242
|
+
// Handle simple variable check
|
|
243
|
+
const simple_match = condition.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
244
|
+
if (simple_match) {
|
|
245
|
+
const value = content[simple_match[1]];
|
|
246
|
+
return !!value;
|
|
247
|
+
}
|
|
248
|
+
// Handle obj.prop patterns
|
|
249
|
+
const prop_match = condition.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.(\w+)$/);
|
|
250
|
+
if (prop_match) {
|
|
251
|
+
const obj = content[prop_match[1]];
|
|
252
|
+
if (!obj || typeof obj !== 'object')
|
|
253
|
+
return false;
|
|
254
|
+
return !!obj[prop_match[2]];
|
|
255
|
+
}
|
|
256
|
+
// Default to checking if any referenced variable is truthy
|
|
257
|
+
const var_match = condition.match(/([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
258
|
+
if (var_match) {
|
|
259
|
+
return !!content[var_match[1]];
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
function process_each_blocks(html, content) {
|
|
264
|
+
// Handle {#each items as item}...{/each}
|
|
265
|
+
const each_regex = /\{#each\s+([^\s]+)\s+as\s+([^}]+)\}([\s\S]*?)\{\/each\}/g;
|
|
266
|
+
return html.replace(each_regex, (match, arrayName, itemName, inner) => {
|
|
267
|
+
const array = content[arrayName.trim()];
|
|
268
|
+
if (!Array.isArray(array))
|
|
269
|
+
return '';
|
|
270
|
+
// Handle "item, index" pattern
|
|
271
|
+
const [item_var, index_var] = itemName.split(',').map((s) => s.trim());
|
|
272
|
+
return array.map((item, index) => {
|
|
273
|
+
let result = inner;
|
|
274
|
+
// Replace {item.prop} patterns
|
|
275
|
+
if (typeof item === 'object' && item !== null) {
|
|
276
|
+
result = result.replace(new RegExp(`\\{${item_var}\\.(\\w+)\\}`, 'g'), (_, prop) => {
|
|
277
|
+
const value = item[prop];
|
|
278
|
+
if (value === undefined || value === null)
|
|
279
|
+
return '';
|
|
280
|
+
return escape_html(String(value));
|
|
281
|
+
});
|
|
282
|
+
// Replace {@html item.prop} patterns
|
|
283
|
+
result = result.replace(new RegExp(`\\{@html\\s+${item_var}\\.(\\w+)\\}`, 'g'), (_, prop) => {
|
|
284
|
+
const value = item[prop];
|
|
285
|
+
if (value === undefined || value === null)
|
|
286
|
+
return '';
|
|
287
|
+
return String(value);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// Replace {item} if item is a primitive
|
|
291
|
+
if (typeof item !== 'object') {
|
|
292
|
+
result = result.replace(new RegExp(`\\{${item_var}\\}`, 'g'), escape_html(String(item)));
|
|
293
|
+
}
|
|
294
|
+
// Replace index variable if present
|
|
295
|
+
if (index_var) {
|
|
296
|
+
result = result.replace(new RegExp(`\\{${index_var}\\}`, 'g'), String(index));
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}).join('');
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function get_nested_value(obj, path) {
|
|
303
|
+
const parts = path.split('.');
|
|
304
|
+
let current = obj;
|
|
305
|
+
for (const part of parts) {
|
|
306
|
+
if (current === null || current === undefined)
|
|
307
|
+
return undefined;
|
|
308
|
+
if (typeof current !== 'object')
|
|
309
|
+
return undefined;
|
|
310
|
+
current = current[part];
|
|
311
|
+
}
|
|
312
|
+
return current;
|
|
313
|
+
}
|
|
314
|
+
function normalize_content(content) {
|
|
315
|
+
// Some content values come as arrays (from CMS), normalize to single values
|
|
316
|
+
const normalized = {};
|
|
317
|
+
for (const [key, value] of Object.entries(content)) {
|
|
318
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
319
|
+
// Use first item if it's an array of primitives or objects
|
|
320
|
+
normalized[key] = value[0];
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
normalized[key] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
function escape_html(str) {
|
|
329
|
+
return str
|
|
330
|
+
.replace(/&/g, '&')
|
|
331
|
+
.replace(/</g, '<')
|
|
332
|
+
.replace(/>/g, '>')
|
|
333
|
+
.replace(/"/g, '"')
|
|
334
|
+
.replace(/'/g, ''');
|
|
335
|
+
}
|
|
336
|
+
function generate_html(site_name, page_name, body, css) {
|
|
337
|
+
const title = page_name === 'Home' ? site_name : `${page_name} | ${site_name}`;
|
|
338
|
+
return `<!DOCTYPE html>
|
|
339
|
+
<html lang="en">
|
|
340
|
+
<head>
|
|
341
|
+
<meta charset="UTF-8">
|
|
342
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
343
|
+
<title>${escape_html(title)}</title>
|
|
344
|
+
<style>
|
|
345
|
+
*, *::before, *::after {
|
|
346
|
+
box-sizing: border-box;
|
|
347
|
+
}
|
|
348
|
+
body {
|
|
349
|
+
margin: 0;
|
|
350
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
351
|
+
line-height: 1.5;
|
|
352
|
+
}
|
|
353
|
+
img {
|
|
354
|
+
max-width: 100%;
|
|
355
|
+
height: auto;
|
|
356
|
+
}
|
|
357
|
+
${css}
|
|
358
|
+
</style>
|
|
359
|
+
</head>
|
|
360
|
+
<body>
|
|
361
|
+
${body}
|
|
362
|
+
</body>
|
|
363
|
+
</html>
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
async function copy_dir(src, dest) {
|
|
367
|
+
await fs.mkdir(dest, { recursive: true });
|
|
368
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const src_path = path.join(src, entry.name);
|
|
371
|
+
const dest_path = path.join(dest, entry.name);
|
|
372
|
+
if (entry.isDirectory()) {
|
|
373
|
+
await copy_dir(src_path, dest_path);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
await fs.copyFile(src_path, dest_path);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|