SafeSight's picture
Simulator
4b3d13d
Raw
History Blame Contribute Delete
8.86 kB
import { BlockDefinition, ProjectFile, VisualElement } from '../types/blocks';
interface CompileOptions {
blocks: BlockDefinition[];
fileTree: ProjectFile[];
visualElements: VisualElement[];
activeFile?: ProjectFile;
projectName: string;
}
export function compileWeb(options: CompileOptions): string {
const { fileTree, activeFile, blocks } = options;
if (!activeFile) {
// Generate default index.html with all visual elements
return generateDefaultHTML(options);
}
switch (activeFile.type) {
case 'html':
return activeFile.content || generateDefaultHTML(options);
case 'css':
return activeFile.content || '/* Styles */\n';
case 'js':
return activeFile.content || '// JavaScript\n';
default:
return activeFile.content || '';
}
}
function findFileInTree(files: ProjectFile[], id: string): ProjectFile | null {
for (const f of files) {
if (f.id === id) return f;
if (f.children) {
const found = findFileInTree(f.children, id);
if (found) return found;
}
}
return null;
}
function generateDefaultHTML(options: CompileOptions): string {
const { visualElements, projectName, fileTree, activeFile } = options;
// Find linked CSS and JS files
const linkedIds = activeFile?.linkedFiles || [];
const linkedFiles = linkedIds.map(id => findFileInTree(fileTree, id)).filter(Boolean) as ProjectFile[];
const linkedCSS = linkedFiles.filter(f => f.type === 'css');
const linkedJS = linkedFiles.filter(f => f.type === 'js');
// Build link and script tags
const cssLinks = linkedCSS.map(f => ` <link rel="stylesheet" href="${f.name}" />`).join('\n');
const jsScripts = linkedJS.map(f => ` <script src="${f.name}"></script>`).join('\n');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${projectName || 'My Web App'}</title>
${cssLinks}
</head>
<body>
${visualElements.map(el => renderVisualElement(el, 0)).join('\n')}
${jsScripts}
</body>
</html>`;
}
export function renderVisualElement(el: VisualElement, depth: number): string {
const indent = ' '.repeat(depth + 1);
const props = Object.entries(el.props)
.filter(([k, v]) => v !== undefined && v !== null && v !== '' && k !== 'textContent' && k !== 'children')
.map(([k, v]) => {
if (typeof v === 'boolean') return v ? k : '';
if (k === 'style') return '';
return `${k}="${String(v).replace(/"/g, '&quot;')}"`;
})
.filter(Boolean)
.join(' ');
const styles = Object.entries(el.styles)
.map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${v}`)
.join('; ');
const styleAttr = styles ? ` style="${styles}"` : '';
const classAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
const propsStr = props ? ' ' + props : '';
const attrs = propsStr + styleAttr + classAttr;
const content = el.props.textContent || '';
if (el.children && el.children.length > 0) {
return `${indent}<${el.tagName}${attrs}>
${el.children.map(c => renderVisualElement(c, depth + 1)).join('\n')}
${indent}</${el.tagName}>`;
}
if (el.tagName === 'img' || el.tagName === 'input' || el.tagName === 'br') {
return `${indent}<${el.tagName}${attrs} />`;
}
return `${indent}<${el.tagName}${attrs}>${content || ''}</${el.tagName}>`;
}
export function compileElectron(options: CompileOptions): string {
const { activeFile } = options;
if (!activeFile) return '// Electron main process\n';
switch (activeFile.type) {
case 'typescript':
return activeFile.content || generateDefaultElectronMain(options);
case 'html':
return activeFile.content || generateDefaultHTML(options);
case 'css':
return activeFile.content || '/* Styles */\n';
default:
return activeFile.content || '';
}
}
function generateDefaultElectronMain(options: CompileOptions): string {
return `import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';
let mainWindow: BrowserWindow | null = null;
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
});
mainWindow.loadFile('index.html');
mainWindow.on('closed', () => { mainWindow = null; });
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
`;
}
export function compileMaui(options: CompileOptions): string {
const { activeFile } = options;
if (!activeFile) return '<!-- .NET MAUI Page -->\n';
switch (activeFile.type) {
case 'xaml':
return activeFile.content || generateDefaultXaml(options);
case 'csharp':
return activeFile.content || generateDefaultCSharp(options);
default:
return activeFile.content || '';
}
}
function generateDefaultXaml(options: CompileOptions): string {
return `<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="${options.projectName}.MainPage"
Title="Main Page"
BackgroundColor="White">
<VerticalStackLayout Spacing="16" Padding="24">
<Label Text="Welcome to ${options.projectName}!"
FontSize="24"
FontAttributes="Bold"
HorizontalTextAlignment="Center" />
<Entry x:Name="inputField"
Placeholder="Enter text here..." />
<Button x:Name="submitButton"
Text="Submit"
BackgroundColor="Blue"
TextColor="White"
Clicked="OnSubmitClicked" />
</VerticalStackLayout>
</ContentPage>`;
}
function generateDefaultCSharp(options: CompileOptions): string {
return `namespace ${options.projectName};
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private void OnSubmitClicked(object sender, EventArgs e)
{
DisplayAlert("Hello", $"You entered: {inputField.Text}", "OK");
}
}`;
}
export function compileNodeJS(options: CompileOptions): string {
const { activeFile } = options;
if (!activeFile) return '// Node.js server\n';
switch (activeFile.type) {
case 'js':
return activeFile.content || generateDefaultExpress(options);
case 'json':
return activeFile.content || generateDefaultPackageJson(options);
case 'env':
return activeFile.content || '# Environment Variables\nPORT=3000\n';
default:
return activeFile.content || '';
}
}
function generateDefaultExpress(options: CompileOptions): string {
return `const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({
message: 'Welcome to ${options.projectName}!',
version: '1.0.0',
});
});
// Start server
app.listen(PORT, () => {
console.log(\`Server running on port \${PORT}\`);
});
`;
}
/**
* Generate a complete HTML document for preview purposes.
* Renders visual elements and inlines linked CSS content.
* Does NOT include script tags — the caller injects those.
*/
export function generatePreviewHtml(options: {
visualElements: VisualElement[];
projectName: string;
linkedCssContent: { content: string }[];
}): string {
const { visualElements, projectName, linkedCssContent } = options;
const cssBlocks = linkedCssContent
.filter(f => f.content)
.map(f => f.content)
.join('\n\n');
const styleTag = cssBlocks
? ` <style>\n${cssBlocks}\n </style>`
: '';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${projectName || 'My Web App'}</title>
${styleTag}
</head>
<body>
${visualElements.map(el => renderVisualElement(el, 0)).join('\n')}
</body>
</html>`;
}
function generateDefaultPackageJson(options: CompileOptions): string {
return JSON.stringify({
name: options.projectName.toLowerCase().replace(/\s+/g, '-'),
version: '1.0.0',
description: `${options.projectName} - built with RealBlocks`,
main: 'server.js',
scripts: {
start: 'node server.js',
dev: 'nodemon server.js',
},
dependencies: {
express: '^4.18.2',
cors: '^2.8.5',
},
devDependencies: {
nodemon: '^3.0.0',
},
}, null, 2);
}