release
This commit is contained in:
commit
47f67eea8c
43 changed files with 5819 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
dist/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
bun.lockb
|
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Blueprint 🎨 - Modern Web UI Language
|
||||
|
||||
A modern programming language, which feels like SwiftUI, but for web development.
|
||||
|
||||
## Quick Start 🚀
|
||||
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone https://github.com/epilogueteam/blueprint.git
|
||||
cd blueprint
|
||||
npm install
|
||||
|
||||
# Development
|
||||
npm run dev # Starts server at http://localhost:3000
|
||||
|
||||
# Production
|
||||
npm run build # Generates production files
|
||||
```
|
||||
|
||||
## Why Blueprint? ✨
|
||||
|
||||
- 🎯 SwiftUI-like syntax for web development
|
||||
- ⚡️ Optimized performance & real-time updates
|
||||
- 🌐 Cross-platform responsive design
|
||||
- 💝 Free & open-source
|
||||
- 🔄 Live reload development
|
||||
|
||||
## Development Guide 💻
|
||||
|
||||
1. After installation, access your project at `http://localhost:3000`
|
||||
2. Make changes and see them instantly with live reload
|
||||
3. For production, build optimized files using `npm run build`
|
||||
4. Deploy built files to your preferred hosting platform
|
||||
|
142
docs/README.md
Normal file
142
docs/README.md
Normal file
|
@ -0,0 +1,142 @@
|
|||
# Blueprint Documentation
|
||||
|
||||
Blueprint is a modern, declarative UI framework for building beautiful web interfaces. It provides a simple, intuitive syntax for creating responsive, dark-themed web applications with consistent styling and behavior.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **Declarative Syntax**: Write UI components using a clean, intuitive syntax
|
||||
- **Dark Theme**: Beautiful dark theme with consistent styling
|
||||
- **Responsive Design**: Built-in responsive design system
|
||||
- **Component-Based**: Rich set of reusable components
|
||||
- **Type-Safe**: Catch errors before they reach the browser
|
||||
- **Custom Properties**: Direct control over styling when needed
|
||||
- **Live Preview**: Changes appear instantly in your browser
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
1. [Getting Started](getting-started.md)
|
||||
- Installation
|
||||
- Basic Usage
|
||||
- Project Structure
|
||||
- Property Types
|
||||
- Page Configuration
|
||||
- Error Handling
|
||||
- Best Practices
|
||||
|
||||
2. [Components](components.md)
|
||||
- Layout Components (Section, Grid, Horizontal, Vertical)
|
||||
- Typography (Title, Text)
|
||||
- Navigation (Navbar, Links)
|
||||
- Form Elements (Input, Textarea, Select, Checkbox, Radio, Switch)
|
||||
- Interactive Components (Button, Card, Badge, Alert, Tooltip)
|
||||
- Container Components (List, Table, Progress, Slider)
|
||||
|
||||
3. [Styling](styling.md)
|
||||
- Layout Properties
|
||||
- Typography Properties
|
||||
- Component Styles
|
||||
- Interactive States
|
||||
- Responsive Design
|
||||
- Custom Properties
|
||||
- Theme Variables
|
||||
- Best Practices
|
||||
|
||||
4. [Examples](examples.md)
|
||||
- Basic Examples
|
||||
- Layout Examples
|
||||
- Form Examples
|
||||
- Navigation Examples
|
||||
- Complete Page Examples
|
||||
|
||||
## Quick Start
|
||||
|
||||
```blueprint
|
||||
page {
|
||||
title { "My First Blueprint Page" }
|
||||
description { "A simple Blueprint page example" }
|
||||
meta-viewport { "width=device-width, initial-scale=1" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal(spaced) {
|
||||
text(bold) { "My App" }
|
||||
links {
|
||||
link(href:/) { "Home" }
|
||||
link(href:/about) { "About" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(gap:2) {
|
||||
title(huge) { "Welcome to Blueprint" }
|
||||
text(subtle) { "Start building beautiful UIs with Blueprint" }
|
||||
|
||||
horizontal(centered, gap:2) {
|
||||
button { "Get Started" }
|
||||
button-light { "Learn More" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
1. **Elements**
|
||||
- Basic building blocks of Blueprint
|
||||
- Each element maps to an HTML tag
|
||||
- Elements can have properties and children
|
||||
- Elements follow semantic naming
|
||||
|
||||
2. **Properties**
|
||||
- Flag properties (e.g., `centered`, `bold`)
|
||||
- Key-value properties (e.g., `type:email`)
|
||||
- Numeric properties (e.g., `width:80`)
|
||||
- Color properties (e.g., `color:#ff0000`)
|
||||
|
||||
3. **Styling**
|
||||
- Consistent dark theme
|
||||
- Built-in responsive design
|
||||
- Direct style properties
|
||||
- Theme variables
|
||||
- Interactive states
|
||||
|
||||
4. **Components**
|
||||
- Layout components
|
||||
- Form elements
|
||||
- Interactive components
|
||||
- Container components
|
||||
- Typography elements
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Organization**
|
||||
- Group related elements
|
||||
- Use consistent spacing
|
||||
- Keep files focused
|
||||
- Split into components
|
||||
|
||||
2. **Styling**
|
||||
- Use predefined properties
|
||||
- Maintain consistency
|
||||
- Leverage built-in features
|
||||
- Custom styles sparingly
|
||||
|
||||
3. **Performance**
|
||||
- Small, focused files
|
||||
- Optimize assets
|
||||
- Use responsive features
|
||||
- Minimize custom styles
|
||||
|
||||
4. **Accessibility**
|
||||
- Semantic elements
|
||||
- Color contrast
|
||||
- Focus states
|
||||
- Screen reader support
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check the [examples](examples.md) for common patterns
|
||||
- Read the [components guide](components.md) for detailed documentation
|
||||
- Learn about styling in the [styling guide](styling.md)
|
||||
- Start with the [getting started guide](getting-started.md) for basics
|
365
docs/components.md
Normal file
365
docs/components.md
Normal file
|
@ -0,0 +1,365 @@
|
|||
# Blueprint Components
|
||||
|
||||
Blueprint provides a rich set of components for building modern web interfaces. Each component is designed to be responsive, accessible, and consistent with the dark theme.
|
||||
|
||||
## Layout Components
|
||||
|
||||
### Section
|
||||
Container for page sections:
|
||||
```blueprint
|
||||
section(wide, centered) {
|
||||
// Content
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `wide`: Full width with max-width constraint (1200px)
|
||||
- `centered`: Center content horizontally and vertically
|
||||
- `alternate`: Alternate background color
|
||||
- `padding`: Custom padding in pixels
|
||||
- `margin`: Custom margin in pixels
|
||||
|
||||
### Grid
|
||||
Responsive grid layout:
|
||||
```blueprint
|
||||
grid(columns:3) {
|
||||
// Grid items
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `columns`: Number of columns (default: auto-fit)
|
||||
- `responsive`: Enable responsive behavior
|
||||
- `gap`: Custom gap size between items
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Horizontal
|
||||
Horizontal flex container:
|
||||
```blueprint
|
||||
horizontal(centered, spaced) {
|
||||
// Horizontal items
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `centered`: Center items vertically
|
||||
- `spaced`: Space between items
|
||||
- `gap`: Custom gap size
|
||||
- `width`: Custom width in percentage
|
||||
- `responsive`: Wrap items on small screens
|
||||
|
||||
### Vertical
|
||||
Vertical flex container:
|
||||
```blueprint
|
||||
vertical(centered) {
|
||||
// Vertical items
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `centered`: Center items horizontally
|
||||
- `spaced`: Space between items
|
||||
- `gap`: Custom gap size
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
## Typography
|
||||
|
||||
### Title
|
||||
Page or section titles:
|
||||
```blueprint
|
||||
title(huge) { "Main Title" }
|
||||
title(large) { "Section Title" }
|
||||
```
|
||||
Properties:
|
||||
- `huge`: Very large size (4rem)
|
||||
- `large`: Large size (2rem)
|
||||
- `bold`: Bold weight
|
||||
- `centered`: Center align
|
||||
- `color`: Custom text color
|
||||
|
||||
### Text
|
||||
Regular text content:
|
||||
```blueprint
|
||||
text(large) { "Large text" }
|
||||
text(subtle) { "Subtle text" }
|
||||
```
|
||||
Properties:
|
||||
- `large`: Larger size
|
||||
- `small`: Smaller size (0.875rem)
|
||||
- `subtle`: Muted color
|
||||
- `bold`: Bold weight
|
||||
- `color`: Custom text color
|
||||
|
||||
## Navigation
|
||||
|
||||
### Navbar
|
||||
Fixed navigation bar:
|
||||
```blueprint
|
||||
navbar {
|
||||
horizontal {
|
||||
text(bold) { "Brand" }
|
||||
links {
|
||||
link(href:home) { "Home" }
|
||||
link(href:about) { "About" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `sticky`: Fixed to top
|
||||
- `transparent`: Transparent background
|
||||
- `backgroundColor`: Custom background color
|
||||
|
||||
### Links
|
||||
Navigation link group:
|
||||
```blueprint
|
||||
links {
|
||||
link(href:page1) { "Link 1" }
|
||||
link(href:page2) { "Link 2" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `spaced`: Add spacing between links
|
||||
- `vertical`: Vertical orientation
|
||||
- `gap`: Custom gap size
|
||||
|
||||
### Link
|
||||
Individual link:
|
||||
```blueprint
|
||||
link(href:page, text:"Click here") { }
|
||||
link(href:https://example.com) { "External Link" }
|
||||
```
|
||||
Properties:
|
||||
- `href`: Target URL or page
|
||||
- `text`: Link text (optional)
|
||||
- `external`: Open in new tab (automatic for http/https URLs)
|
||||
- `color`: Custom text color
|
||||
|
||||
## Interactive Components
|
||||
|
||||
### Button
|
||||
Various button styles:
|
||||
```blueprint
|
||||
button { "Primary" }
|
||||
button-secondary { "Secondary" }
|
||||
button-light { "Light" }
|
||||
button-compact { "Compact" }
|
||||
```
|
||||
Properties:
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
- `backgroundColor`: Custom background color
|
||||
- `color`: Custom text color
|
||||
|
||||
### Card
|
||||
Content container with hover effect:
|
||||
```blueprint
|
||||
card {
|
||||
title { "Card Title" }
|
||||
text { "Card content" }
|
||||
button { "Action" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `raised`: Add shadow and hover effect
|
||||
- `width`: Custom width in percentage
|
||||
- `padding`: Custom padding in pixels
|
||||
- `backgroundColor`: Custom background color
|
||||
|
||||
### Badge
|
||||
Status indicators:
|
||||
```blueprint
|
||||
badge { "New" }
|
||||
badge(color:blue) { "Status" }
|
||||
```
|
||||
Properties:
|
||||
- `color`: Custom badge color
|
||||
- `backgroundColor`: Custom background color
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Alert
|
||||
Notification messages:
|
||||
```blueprint
|
||||
alert(type:info) { "Information message" }
|
||||
```
|
||||
Properties:
|
||||
- `type`: info, success, warning, error
|
||||
- `backgroundColor`: Custom background color
|
||||
- `color`: Custom text color
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Tooltip
|
||||
Hover tooltips:
|
||||
```blueprint
|
||||
tooltip(text:"More info") {
|
||||
text { "Hover me" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `text`: Tooltip text
|
||||
- `position`: top, right, bottom, left
|
||||
- `backgroundColor`: Custom background color
|
||||
- `color`: Custom text color
|
||||
|
||||
## Form Elements
|
||||
|
||||
### Input
|
||||
Text input field:
|
||||
```blueprint
|
||||
input(placeholder:"Type here") { }
|
||||
```
|
||||
Properties:
|
||||
- `placeholder`: Placeholder text
|
||||
- `type`: Input type (text, email, password, etc.)
|
||||
- `required`: Required field
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Textarea
|
||||
Multi-line text input:
|
||||
```blueprint
|
||||
textarea(placeholder:"Enter message") { }
|
||||
```
|
||||
Properties:
|
||||
- `placeholder`: Placeholder text
|
||||
- `rows`: Number of visible rows
|
||||
- `required`: Required field
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Select
|
||||
Dropdown selection:
|
||||
```blueprint
|
||||
select {
|
||||
option { "Option 1" }
|
||||
option { "Option 2" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `placeholder`: Placeholder text
|
||||
- `required`: Required field
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Checkbox
|
||||
Checkbox input:
|
||||
```blueprint
|
||||
horizontal {
|
||||
checkbox { }
|
||||
text { "Accept terms" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `checked`: Default checked state
|
||||
- `required`: Required field
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Radio
|
||||
Radio button input:
|
||||
```blueprint
|
||||
vertical {
|
||||
horizontal {
|
||||
radio(name:"choice") { }
|
||||
text { "Option 1" }
|
||||
}
|
||||
horizontal {
|
||||
radio(name:"choice") { }
|
||||
text { "Option 2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `name`: Group name
|
||||
- `checked`: Default checked state
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Switch
|
||||
Toggle switch:
|
||||
```blueprint
|
||||
horizontal {
|
||||
switch { }
|
||||
text { "Enable feature" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `checked`: Default checked state
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
## Container Components
|
||||
|
||||
### List
|
||||
Ordered or unordered lists:
|
||||
```blueprint
|
||||
list {
|
||||
text { "Item 1" }
|
||||
text { "Item 2" }
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `ordered`: Use ordered list
|
||||
- `bullet`: Show bullets
|
||||
- `spaced`: Add spacing
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Table
|
||||
Data tables:
|
||||
```blueprint
|
||||
table {
|
||||
row {
|
||||
cell { "Header 1" }
|
||||
cell { "Header 2" }
|
||||
}
|
||||
row {
|
||||
cell { "Data 1" }
|
||||
cell { "Data 2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
Properties:
|
||||
- `striped`: Alternate row colors
|
||||
- `bordered`: Add borders
|
||||
- `compact`: Reduced padding
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Progress
|
||||
Progress indicators:
|
||||
```blueprint
|
||||
progress(value:75, max:100) { }
|
||||
```
|
||||
Properties:
|
||||
- `value`: Current value
|
||||
- `max`: Maximum value
|
||||
- `indeterminate`: Loading state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Slider
|
||||
Range input:
|
||||
```blueprint
|
||||
slider(min:0, max:100, value:50) { }
|
||||
```
|
||||
Properties:
|
||||
- `min`: Minimum value
|
||||
- `max`: Maximum value
|
||||
- `step`: Step increment
|
||||
- `disabled`: Disabled state
|
||||
- `width`: Custom width in percentage
|
||||
|
||||
### Media
|
||||
Images and videos with responsive behavior:
|
||||
```blueprint
|
||||
media(src:/path/to/image.jpg) { "Image description" }
|
||||
media(src:https://example.com/video.mp4, type:video) { "Video description" }
|
||||
```
|
||||
Properties:
|
||||
- `src`: URL or path to the media file (required)
|
||||
- `type`: Media type (`img` or `video`, defaults to `img`)
|
||||
- `width`: Custom width in percentage
|
||||
- `height`: Custom height in percentage
|
||||
- `padding`: Custom padding in pixels
|
||||
- `margin`: Custom margin in pixels
|
||||
|
||||
The media component automatically:
|
||||
- Scales images/videos responsively (max-width: 100%)
|
||||
- Maintains aspect ratio (height: auto)
|
||||
- Adds rounded corners
|
||||
- Includes a subtle hover effect
|
||||
- Uses the content as alt text for images
|
||||
- Adds video controls when type is video
|
375
docs/examples.md
Normal file
375
docs/examples.md
Normal file
|
@ -0,0 +1,375 @@
|
|||
# Blueprint Examples
|
||||
|
||||
This guide provides comprehensive examples of common UI patterns and layouts using Blueprint.
|
||||
|
||||
## Basic Examples
|
||||
|
||||
### Page Setup
|
||||
```blueprint
|
||||
page {
|
||||
title { "My Blueprint Page" }
|
||||
description { "A comprehensive example page" }
|
||||
keywords { "blueprint, example, ui" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
text(bold) { "My App" }
|
||||
links {
|
||||
link(href:home) { "Home" }
|
||||
link(href:about) { "About" }
|
||||
link(href:contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
title(huge) { "Welcome" }
|
||||
text(subtle) { "Start building beautiful UIs" }
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Card
|
||||
```blueprint
|
||||
card {
|
||||
title { "Simple Card" }
|
||||
text { "This is a basic card with some content." }
|
||||
button { "Learn More" }
|
||||
}
|
||||
```
|
||||
|
||||
### Alert Messages
|
||||
```blueprint
|
||||
vertical(gap:2) {
|
||||
alert(type:info) { "This is an information message" }
|
||||
alert(type:success) { "Operation completed successfully" }
|
||||
alert(type:warning) { "Please review your input" }
|
||||
alert(type:error) { "An error occurred" }
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Examples
|
||||
|
||||
### Grid Layout
|
||||
```blueprint
|
||||
section(wide) {
|
||||
title { "Our Features" }
|
||||
|
||||
grid(columns:3) {
|
||||
card {
|
||||
title { "Feature 1" }
|
||||
text { "Description of feature 1" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
card {
|
||||
title { "Feature 2" }
|
||||
text { "Description of feature 2" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
card {
|
||||
title { "Feature 3" }
|
||||
text { "Description of feature 3" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive Layout
|
||||
```blueprint
|
||||
section(wide) {
|
||||
horizontal(mobile-stack) {
|
||||
vertical(width:40) {
|
||||
title { "Left Column" }
|
||||
text { "This column takes 40% width on desktop" }
|
||||
}
|
||||
vertical(width:60) {
|
||||
title { "Right Column" }
|
||||
text { "This column takes 60% width on desktop" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Layout
|
||||
```blueprint
|
||||
section(wide) {
|
||||
vertical(centered) {
|
||||
title(huge) { "Nested Layout" }
|
||||
|
||||
horizontal(centered, gap:4) {
|
||||
vertical(centered) {
|
||||
title { "Column 1" }
|
||||
text { "Content" }
|
||||
}
|
||||
vertical(centered) {
|
||||
title { "Column 2" }
|
||||
text { "Content" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Form Examples
|
||||
|
||||
### Login Form
|
||||
```blueprint
|
||||
section(wide, centered) {
|
||||
card {
|
||||
title { "Login" }
|
||||
vertical(gap:2) {
|
||||
vertical {
|
||||
text(bold) { "Email" }
|
||||
input(type:email, placeholder:"Enter your email") { }
|
||||
}
|
||||
vertical {
|
||||
text(bold) { "Password" }
|
||||
input(type:password, placeholder:"Enter your password") { }
|
||||
}
|
||||
horizontal {
|
||||
checkbox { }
|
||||
text { "Remember me" }
|
||||
}
|
||||
button { "Sign In" }
|
||||
text(small, centered) { "Forgot password?" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contact Form
|
||||
```blueprint
|
||||
section(wide) {
|
||||
card {
|
||||
title { "Contact Us" }
|
||||
vertical(gap:2) {
|
||||
horizontal(gap:2) {
|
||||
vertical {
|
||||
text(bold) { "First Name" }
|
||||
input(placeholder:"John") { }
|
||||
}
|
||||
vertical {
|
||||
text(bold) { "Last Name" }
|
||||
input(placeholder:"Doe") { }
|
||||
}
|
||||
}
|
||||
vertical {
|
||||
text(bold) { "Email" }
|
||||
input(type:email, placeholder:"john@example.com") { }
|
||||
}
|
||||
vertical {
|
||||
text(bold) { "Message" }
|
||||
textarea(placeholder:"Your message here...") { }
|
||||
}
|
||||
button { "Send Message" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Form
|
||||
```blueprint
|
||||
section(wide) {
|
||||
card {
|
||||
title { "Settings" }
|
||||
vertical(gap:3) {
|
||||
horizontal {
|
||||
vertical(width:70) {
|
||||
title(small) { "Notifications" }
|
||||
text(subtle) { "Manage your notification preferences" }
|
||||
}
|
||||
switch { }
|
||||
}
|
||||
horizontal {
|
||||
vertical(width:70) {
|
||||
title(small) { "Dark Mode" }
|
||||
text(subtle) { "Toggle dark/light theme" }
|
||||
}
|
||||
switch { }
|
||||
}
|
||||
horizontal {
|
||||
vertical(width:70) {
|
||||
title(small) { "Email Updates" }
|
||||
text(subtle) { "Receive email updates about your account" }
|
||||
}
|
||||
switch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Navigation Examples
|
||||
|
||||
### Complex Navbar
|
||||
```blueprint
|
||||
navbar {
|
||||
horizontal {
|
||||
horizontal(gap:2) {
|
||||
text(bold) { "Logo" }
|
||||
links {
|
||||
link(href:home) { "Home" }
|
||||
link(href:products) { "Products" }
|
||||
link(href:pricing) { "Pricing" }
|
||||
link(href:about) { "About" }
|
||||
}
|
||||
}
|
||||
horizontal(gap:2) {
|
||||
button-light { "Sign In" }
|
||||
button { "Get Started" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sidebar Navigation
|
||||
```blueprint
|
||||
horizontal {
|
||||
vertical(width:20) {
|
||||
title { "Dashboard" }
|
||||
links(vertical) {
|
||||
link(href:home) { "Home" }
|
||||
link(href:profile) { "Profile" }
|
||||
link(href:settings) { "Settings" }
|
||||
link(href:help) { "Help" }
|
||||
}
|
||||
}
|
||||
vertical(width:80) {
|
||||
title { "Main Content" }
|
||||
text { "Your content here" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Breadcrumb Navigation
|
||||
```blueprint
|
||||
horizontal(gap:1) {
|
||||
link(href:home) { "Home" }
|
||||
text { ">" }
|
||||
link(href:products) { "Products" }
|
||||
text { ">" }
|
||||
text(bold) { "Current Page" }
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Page Examples
|
||||
|
||||
### Landing Page
|
||||
```blueprint
|
||||
page {
|
||||
title { "Blueprint - Modern UI Framework" }
|
||||
description { "Build beautiful web interfaces with Blueprint" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
text(bold) { "Blueprint" }
|
||||
links {
|
||||
link(href:features) { "Features" }
|
||||
link(href:docs) { "Docs" }
|
||||
link(href:pricing) { "Pricing" }
|
||||
button { "Get Started" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(centered) {
|
||||
title(huge) { "Build Beautiful UIs" }
|
||||
text(large, subtle) { "Create modern web interfaces with ease" }
|
||||
horizontal(centered, gap:2) {
|
||||
button { "Get Started" }
|
||||
button-light { "Learn More" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
grid(columns:3) {
|
||||
card {
|
||||
title { "Easy to Use" }
|
||||
text { "Simple, declarative syntax for building UIs" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
card {
|
||||
title { "Modern Design" }
|
||||
text { "Beautiful dark theme with consistent styling" }
|
||||
button-secondary { "View Examples" }
|
||||
}
|
||||
card {
|
||||
title { "Responsive" }
|
||||
text { "Looks great on all devices out of the box" }
|
||||
button-secondary { "See Details" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dashboard Page
|
||||
```blueprint
|
||||
page {
|
||||
title { "Dashboard - My App" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
text(bold) { "Dashboard" }
|
||||
horizontal {
|
||||
text { "Welcome back, " }
|
||||
text(bold) { "John" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
grid(columns:4) {
|
||||
card {
|
||||
title { "Total Users" }
|
||||
text(huge) { "1,234" }
|
||||
text(subtle) { "+12% this month" }
|
||||
}
|
||||
card {
|
||||
title { "Revenue" }
|
||||
text(huge) { "$5,678" }
|
||||
text(subtle) { "+8% this month" }
|
||||
}
|
||||
card {
|
||||
title { "Active Users" }
|
||||
text(huge) { "892" }
|
||||
text(subtle) { "Currently online" }
|
||||
}
|
||||
card {
|
||||
title { "Conversion" }
|
||||
text(huge) { "3.2%" }
|
||||
text(subtle) { "+0.8% this month" }
|
||||
}
|
||||
}
|
||||
|
||||
horizontal(gap:4) {
|
||||
vertical(width:60) {
|
||||
card {
|
||||
title { "Recent Activity" }
|
||||
list {
|
||||
text { "User signup: John Doe" }
|
||||
text { "New order: #12345" }
|
||||
text { "Payment received: $99" }
|
||||
}
|
||||
}
|
||||
}
|
||||
vertical(width:40) {
|
||||
card {
|
||||
title { "Quick Actions" }
|
||||
vertical(gap:2) {
|
||||
button { "Create User" }
|
||||
button-secondary { "View Reports" }
|
||||
button-light { "Export Data" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These examples demonstrate common UI patterns and how to implement them using Blueprint. Use them as a starting point for your own projects and customize them to match your needs.
|
213
docs/getting-started.md
Normal file
213
docs/getting-started.md
Normal file
|
@ -0,0 +1,213 @@
|
|||
# Getting Started with Blueprint
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/blueprint.git
|
||||
cd blueprint
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Create your first Blueprint file:
|
||||
```bash
|
||||
mkdir src
|
||||
touch src/index.bp
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Blueprint uses a declarative syntax to define UI components. Each file with a `.bp` extension will be compiled into HTML and CSS.
|
||||
|
||||
### Basic Structure
|
||||
|
||||
A Blueprint file consists of elements, which can have properties and children. Properties can be flags or key-value pairs:
|
||||
|
||||
```blueprint
|
||||
element(flag1, key:value) {
|
||||
child-element {
|
||||
// Content
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Property Types
|
||||
|
||||
Blueprint supports several types of properties:
|
||||
|
||||
1. **Flag Properties**
|
||||
```blueprint
|
||||
button(bold, centered) { "Text" }
|
||||
```
|
||||
|
||||
2. **Key-Value Properties**
|
||||
```blueprint
|
||||
input(type:email, placeholder:"Enter email")
|
||||
```
|
||||
|
||||
3. **Numeric Properties**
|
||||
```blueprint
|
||||
section(width:80, padding:20)
|
||||
```
|
||||
|
||||
4. **Color Properties**
|
||||
```blueprint
|
||||
text(color:#ff0000) { "Red text" }
|
||||
```
|
||||
|
||||
### Page Configuration
|
||||
|
||||
Every Blueprint page can have metadata defined using the `page` element:
|
||||
|
||||
```blueprint
|
||||
page {
|
||||
title { "My Page Title" }
|
||||
description { "Page description for SEO" }
|
||||
keywords { "keyword1, keyword2, keyword3" }
|
||||
author { "Author Name" }
|
||||
meta-viewport { "width=device-width, initial-scale=1" }
|
||||
}
|
||||
```
|
||||
|
||||
Available page metadata:
|
||||
- `title`: Page title (appears in browser tab)
|
||||
- `description`: Meta description for SEO
|
||||
- `keywords`: Meta keywords for SEO
|
||||
- `author`: Page author
|
||||
- `meta-*`: Custom meta tags (e.g., meta-viewport, meta-robots)
|
||||
|
||||
### Basic Layout
|
||||
|
||||
A typical page structure:
|
||||
|
||||
```blueprint
|
||||
page {
|
||||
title { "My First Page" }
|
||||
description { "A simple Blueprint page" }
|
||||
meta-viewport { "width=device-width, initial-scale=1" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal(spaced) {
|
||||
text(bold) { "My App" }
|
||||
links {
|
||||
link(href:/) { "Home" }
|
||||
link(href:/about) { "About" }
|
||||
link(href:/contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(gap:2) {
|
||||
title(huge) { "Welcome to Blueprint" }
|
||||
text(subtle) { "Start building beautiful UIs with Blueprint" }
|
||||
|
||||
horizontal(centered, gap:2) {
|
||||
button { "Get Started" }
|
||||
button-light { "Learn More" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
grid(columns:3) {
|
||||
card {
|
||||
title { "Feature 1" }
|
||||
text { "Description of feature 1" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
card {
|
||||
title { "Feature 2" }
|
||||
text { "Description of feature 2" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
card {
|
||||
title { "Feature 3" }
|
||||
text { "Description of feature 3" }
|
||||
button-secondary { "Learn More" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
A typical Blueprint project has the following structure:
|
||||
|
||||
```
|
||||
my-blueprint-project/
|
||||
├── src/ # Source Blueprint files
|
||||
│ ├── index.bp # Main page
|
||||
│ ├── about.bp # About page
|
||||
│ └── contact.bp # Contact page
|
||||
├── public/ # Static assets
|
||||
│ ├── images/ # Image files
|
||||
│ ├── fonts/ # Font files
|
||||
│ └── favicon.ico # Favicon
|
||||
├── dist/ # Generated files (auto-generated)
|
||||
│ ├── index.html # Compiled HTML
|
||||
│ ├── index.css # Generated CSS
|
||||
│ └── ...
|
||||
└── package.json # Project configuration
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Blueprint provides helpful error messages when something goes wrong:
|
||||
|
||||
```
|
||||
BlueprintError at line 5, column 10:
|
||||
Unknown element type: invalid-element
|
||||
```
|
||||
|
||||
Common errors include:
|
||||
- Missing closing braces
|
||||
- Unknown element types
|
||||
- Invalid property values
|
||||
- Unterminated strings
|
||||
- Missing required properties
|
||||
- Invalid color values
|
||||
- Invalid numeric values
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Organization**
|
||||
- Group related elements logically
|
||||
- Use consistent spacing and indentation
|
||||
- Keep files focused on a single purpose
|
||||
- Split large files into components
|
||||
|
||||
2. **Naming**
|
||||
- Use descriptive names for links and sections
|
||||
- Follow a consistent naming convention
|
||||
- Use semantic element names
|
||||
|
||||
3. **Layout**
|
||||
- Use semantic elements (`section`, `navbar`, etc.)
|
||||
- Leverage the grid system for responsive layouts
|
||||
- Use appropriate spacing with `gap` property
|
||||
- Use `width` and `padding` for fine-tuned control
|
||||
|
||||
4. **Styling**
|
||||
- Use predefined style properties when possible
|
||||
- Group related styles together
|
||||
- Keep styling consistent across pages
|
||||
- Use custom properties sparingly
|
||||
|
||||
5. **Performance**
|
||||
- Keep files small and focused
|
||||
- Use appropriate image formats and sizes
|
||||
- Minimize custom styles
|
||||
- Leverage built-in responsive features
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore available [components](components.md)
|
||||
- Learn about [styling](styling.md)
|
||||
- Check out [examples](examples.md)
|
||||
- Read about advanced features in the component documentation
|
233
docs/styling.md
Normal file
233
docs/styling.md
Normal file
|
@ -0,0 +1,233 @@
|
|||
# Blueprint Styling Guide
|
||||
|
||||
Blueprint provides a comprehensive styling system that ensures consistent, beautiful dark-themed UIs. This guide covers all available styling properties and how to use them effectively.
|
||||
|
||||
## Style Properties
|
||||
|
||||
### Layout Properties
|
||||
|
||||
#### Spacing and Sizing
|
||||
- `wide`: Full width with max-width constraint (1200px)
|
||||
- `compact`: Reduced padding (0.75rem)
|
||||
- `spaced`: Space between items (gap: 1.5rem)
|
||||
- `gap`: Custom gap size between items
|
||||
- `width`: Custom width in percentage
|
||||
- `height`: Custom height in percentage
|
||||
- `padding`: Custom padding in pixels
|
||||
- `margin`: Custom margin in pixels
|
||||
|
||||
#### Positioning
|
||||
- `centered`: Center content horizontally and vertically
|
||||
- `sticky`: Fixed position at top with blur backdrop
|
||||
- `fixed`: Fixed position
|
||||
- `relative`: Relative positioning
|
||||
- `absolute`: Absolute positioning
|
||||
|
||||
#### Display and Flex
|
||||
- `horizontal`: Horizontal flex layout with 1.5rem gap
|
||||
- `vertical`: Vertical flex layout with 1.5rem gap
|
||||
- `grid`: Grid layout with auto-fit columns
|
||||
- `responsive`: Enable responsive wrapping
|
||||
- `hidden`: Hide element
|
||||
- `visible`: Show element
|
||||
|
||||
### Typography Properties
|
||||
|
||||
#### Text Size
|
||||
- `huge`: Very large text (clamp(2.5rem, 5vw, 4rem))
|
||||
- `large`: Large text (clamp(1.5rem, 3vw, 2rem))
|
||||
- `small`: Small text (0.875rem)
|
||||
- `tiny`: Very small text (0.75rem)
|
||||
|
||||
#### Text Weight
|
||||
- `bold`: Bold weight (600)
|
||||
- `light`: Light weight
|
||||
- `normal`: Normal weight
|
||||
|
||||
#### Text Style
|
||||
- `italic`: Italic text
|
||||
- `underline`: Underlined text
|
||||
- `strike`: Strikethrough text
|
||||
- `uppercase`: All uppercase
|
||||
- `lowercase`: All lowercase
|
||||
- `capitalize`: Capitalize first letter
|
||||
|
||||
#### Text Color
|
||||
- `subtle`: Muted text color (#8b949e)
|
||||
- `accent`: Accent color text (#3b82f6)
|
||||
- `error`: Error color text
|
||||
- `success`: Success color text
|
||||
- `warning`: Warning color text
|
||||
- Custom colors using `color:value`
|
||||
|
||||
### Component Styles
|
||||
|
||||
#### Button Styles
|
||||
- `prominent`: Primary button style
|
||||
- Background: #3b82f6
|
||||
- Hover: Scale up and glow
|
||||
- `secondary`: Secondary button style
|
||||
- Background: #1f2937
|
||||
- Hover: Slight raise
|
||||
- `light`: Light button style
|
||||
- Background: Transparent
|
||||
- Border: 1px solid rgba(48, 54, 61, 0.6)
|
||||
- `compact`: Compact button style
|
||||
- Padding: 0.75rem
|
||||
- Border-radius: 12px
|
||||
|
||||
#### Card Styles
|
||||
- `raised`: Card with hover effect
|
||||
- Background: #111827
|
||||
- Border: 1px solid rgba(48, 54, 61, 0.6)
|
||||
- Hover: Raise and glow
|
||||
- `interactive`: Interactive card style
|
||||
- Hover: Scale and border color change
|
||||
|
||||
#### Input Styles
|
||||
- `input`: Standard input style
|
||||
- Background: #111827
|
||||
- Border: 1px solid rgba(48, 54, 61, 0.6)
|
||||
- Focus: Blue glow
|
||||
- `textarea`: Textarea style
|
||||
- Min-height: 120px
|
||||
- Resize: vertical
|
||||
- `select`: Select input style
|
||||
- Custom dropdown arrow
|
||||
- Focus: Blue glow
|
||||
- `checkbox`: Checkbox style
|
||||
- Custom checkmark
|
||||
- Hover: Blue border
|
||||
- `radio`: Radio button style
|
||||
- Custom radio dot
|
||||
- Hover: Blue border
|
||||
- `switch`: Toggle switch style
|
||||
- Animated toggle
|
||||
- Checked: Blue background
|
||||
|
||||
### Interactive States
|
||||
|
||||
#### Hover Effects
|
||||
```blueprint
|
||||
button(hover-scale) { "Scale on Hover" }
|
||||
link(hover-underline) { "Underline on Hover" }
|
||||
card(hover-raise) { "Raise on Hover" }
|
||||
```
|
||||
|
||||
Available hover properties:
|
||||
- `hover-scale`: Scale up on hover (1.1)
|
||||
- `hover-raise`: Raise with shadow
|
||||
- `hover-glow`: Glow effect
|
||||
- `hover-underline`: Underline on hover
|
||||
- `hover-fade`: Fade effect
|
||||
|
||||
#### Focus States
|
||||
```blueprint
|
||||
input(focus-glow) { }
|
||||
button(focus-outline) { "Click me" }
|
||||
```
|
||||
|
||||
Available focus properties:
|
||||
- `focus-glow`: Blue glow effect
|
||||
- `focus-outline`: Blue outline
|
||||
- `focus-scale`: Scale effect
|
||||
|
||||
#### Active States
|
||||
```blueprint
|
||||
button(active-scale) { "Click me" }
|
||||
link(active-color) { "Click me" }
|
||||
```
|
||||
|
||||
Available active properties:
|
||||
- `active-scale`: Scale down
|
||||
- `active-color`: Color change
|
||||
- `active-raise`: Raise effect
|
||||
|
||||
### Responsive Design
|
||||
|
||||
#### Breakpoints
|
||||
Blueprint automatically handles responsive design, but you can use specific properties:
|
||||
|
||||
```blueprint
|
||||
section(mobile-stack) {
|
||||
horizontal(tablet-wrap) {
|
||||
card(desktop-wide) { }
|
||||
card(desktop-wide) { }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Available responsive properties:
|
||||
- `mobile-stack`: Stack elements on mobile
|
||||
- `mobile-hide`: Hide on mobile
|
||||
- `tablet-wrap`: Wrap on tablet
|
||||
- `tablet-hide`: Hide on tablet
|
||||
- `desktop-wide`: Full width on desktop
|
||||
- `desktop-hide`: Hide on desktop
|
||||
|
||||
#### Grid System
|
||||
The grid system automatically adjusts based on screen size:
|
||||
|
||||
```blueprint
|
||||
grid(columns:3, responsive) {
|
||||
card { }
|
||||
card { }
|
||||
card { }
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Properties
|
||||
|
||||
#### Direct Style Properties
|
||||
You can use these properties directly:
|
||||
- `width`: Set width in percentage (e.g., width:80)
|
||||
- `height`: Set height in percentage (e.g., height:50)
|
||||
- `padding`: Set padding in pixels (e.g., padding:20)
|
||||
- `margin`: Set margin in pixels (e.g., margin:10)
|
||||
- `color`: Set text color (e.g., color:#ffffff)
|
||||
- `backgroundColor`: Set background color (e.g., backgroundColor:#000000)
|
||||
|
||||
### Theme Variables
|
||||
|
||||
Blueprint uses CSS variables for consistent theming:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--navbar-height: 4rem;
|
||||
--primary-color: #3b82f6;
|
||||
--secondary-color: #1f2937;
|
||||
--text-color: #e6edf3;
|
||||
--subtle-color: #8b949e;
|
||||
--border-color: rgba(48, 54, 61, 0.6);
|
||||
--background-color: #0d1117;
|
||||
--hover-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Consistency**
|
||||
- Use predefined properties when possible
|
||||
- Maintain consistent spacing
|
||||
- Follow the color theme
|
||||
- Use semantic styles
|
||||
|
||||
2. **Responsive Design**
|
||||
- Test at all breakpoints
|
||||
- Use relative units
|
||||
- Consider mobile-first
|
||||
- Use grid and flex layouts
|
||||
|
||||
3. **Performance**
|
||||
- Minimize custom styles
|
||||
- Use system properties
|
||||
- Avoid deep nesting
|
||||
- Optimize animations
|
||||
|
||||
4. **Accessibility**
|
||||
- Maintain color contrast
|
||||
- Use semantic markup
|
||||
- Consider focus states
|
||||
- Test with screen readers
|
||||
```
|
||||
</rewritten_file>
|
57
examples/blueprint-home/about.bp
Normal file
57
examples/blueprint-home/about.bp
Normal file
|
@ -0,0 +1,57 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - About" }
|
||||
description { "A modern, declarative UI framework for building beautiful web interfaces" }
|
||||
keywords { "blueprint, ui, framework, web development" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint Live" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:components) { "Components" }
|
||||
link(href:about) { "About" }
|
||||
link(href:contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "About Blueprint" }
|
||||
text(subtle) { "A modern UI compiler with live reload support" }
|
||||
|
||||
vertical {
|
||||
card {
|
||||
title { "Our Story" }
|
||||
text { "Blueprint was created to make UI development faster and more enjoyable. With live reload support, you can see your changes instantly without manual refreshing." }
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Features" }
|
||||
vertical {
|
||||
horizontal {
|
||||
badge { "New" }
|
||||
text { "Live Reload Support" }
|
||||
}
|
||||
horizontal {
|
||||
badge { "✨" }
|
||||
text { "Modern Dark Theme" }
|
||||
}
|
||||
horizontal {
|
||||
badge { "🚀" }
|
||||
text { "Fast Development" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Get Started" }
|
||||
text { "Try Blueprint now with our development server:" }
|
||||
codeblock {
|
||||
"node dev.js --live --readable"
|
||||
}
|
||||
link(text:"Get Help") { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
152
examples/blueprint-home/components.bp
Normal file
152
examples/blueprint-home/components.bp
Normal file
|
@ -0,0 +1,152 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Components" }
|
||||
description { "A modern, declarative UI framework for building beautiful web interfaces" }
|
||||
keywords { "blueprint, ui, framework, web development" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint Live" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:components) { "Components" }
|
||||
link(href:about) { "About" }
|
||||
link(href:contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
title(huge) { "Modern UI Components" }
|
||||
text(large, subtle) { "A showcase of beautiful, dark-themed UI elements" }
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Form Elements" }
|
||||
|
||||
grid(columns:2) {
|
||||
card {
|
||||
title { "Text Inputs" }
|
||||
vertical {
|
||||
text(subtle) { "Regular input:" }
|
||||
input { "Type something..." }
|
||||
|
||||
text(subtle) { "Textarea:" }
|
||||
textarea { "Multiple lines of text..." }
|
||||
|
||||
text(subtle) { "Select dropdown:" }
|
||||
select {
|
||||
"Option 1"
|
||||
"Option 2"
|
||||
"Option 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Toggle Controls" }
|
||||
vertical {
|
||||
horizontal {
|
||||
checkbox { }
|
||||
text { "Enable notifications" }
|
||||
}
|
||||
|
||||
horizontal {
|
||||
radio { }
|
||||
text { "Light theme" }
|
||||
}
|
||||
|
||||
horizontal {
|
||||
radio { }
|
||||
text { "Dark theme" }
|
||||
}
|
||||
|
||||
horizontal {
|
||||
switch { }
|
||||
text { "Airplane mode" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Progress Elements" }
|
||||
|
||||
grid(columns:2) {
|
||||
card {
|
||||
title { "Progress Indicators" }
|
||||
vertical {
|
||||
text(subtle) { "Download progress:" }
|
||||
progress(value:75, max:100) { }
|
||||
|
||||
text(subtle) { "Volume control:" }
|
||||
slider(value:50, min:0, max:100) { }
|
||||
}
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Status Indicators" }
|
||||
vertical {
|
||||
horizontal(spaced) {
|
||||
badge { "New" }
|
||||
badge { "Updated" }
|
||||
badge { "Popular" }
|
||||
}
|
||||
|
||||
alert { "✨ Welcome to the new UI Kit!" }
|
||||
|
||||
horizontal {
|
||||
tooltip(data-tooltip:"Click to learn more") {
|
||||
text { "Hover me" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Button Variations" }
|
||||
|
||||
grid(columns:3) {
|
||||
card {
|
||||
title { "Primary Actions" }
|
||||
vertical {
|
||||
button(prominent) { "Save Changes" }
|
||||
button { "Cancel" }
|
||||
}
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Button Groups" }
|
||||
horizontal {
|
||||
button(prominent) { "Previous" }
|
||||
button(prominent) { "Next" }
|
||||
}
|
||||
}
|
||||
|
||||
card {
|
||||
title { "Icon Buttons" }
|
||||
horizontal {
|
||||
button(compact) { "👍" }
|
||||
button(compact) { "❤️" }
|
||||
button(compact) { "🔔" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, alternate) {
|
||||
title(centered) { "Ready to Build?" }
|
||||
|
||||
vertical(centered) {
|
||||
text(large, subtle) { "Start creating your own modern UI today" }
|
||||
|
||||
horizontal(centered, spaced) {
|
||||
button(prominent) { "Get Started" }
|
||||
button(prominent) { "View Documentation" }
|
||||
}
|
||||
}
|
||||
}
|
54
examples/blueprint-home/contact.bp
Normal file
54
examples/blueprint-home/contact.bp
Normal file
|
@ -0,0 +1,54 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Contact" }
|
||||
description { "A modern, declarative UI framework for building beautiful web interfaces" }
|
||||
keywords { "blueprint, ui, framework, web development" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint Live" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:components) { "Components" }
|
||||
link(href:about) { "About" }
|
||||
link(href:contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(vertical, centered) {
|
||||
card(width:100) {
|
||||
title { "Contact Us" }
|
||||
text(subtle) { "Get in touch with the Blueprint team" }
|
||||
|
||||
vertical {
|
||||
text(subtle) { "Name" }
|
||||
input { "Your name" }
|
||||
|
||||
text(subtle) { "Email" }
|
||||
input { "you@example.com" }
|
||||
|
||||
text(subtle) { "Message" }
|
||||
textarea { "Type your message here..." }
|
||||
|
||||
horizontal {
|
||||
checkbox { }
|
||||
text { "Subscribe to updates" }
|
||||
}
|
||||
|
||||
link(href:submit) { button { "Send Message" } }
|
||||
}
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
text(subtle) { "Or connect with us on social media:" }
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
link(href:twitter) { button-compact { "𝕏" } }
|
||||
link(href:facebook) { button-compact { "📘" } }
|
||||
link(href:linkedin) { button-compact { "💼" } }
|
||||
link(href:instagram) { button-compact { "📱" } }
|
||||
}
|
||||
}
|
50
examples/blueprint-home/index.bp
Normal file
50
examples/blueprint-home/index.bp
Normal file
|
@ -0,0 +1,50 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Modern UI Framework" }
|
||||
description { "A modern, declarative UI framework for building beautiful web interfaces" }
|
||||
keywords { "blueprint, ui, framework, web development" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint Live" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:components) { "Components" }
|
||||
link(href:about) { "About" }
|
||||
link(href:contact) { "Contact" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(centered,wide) {
|
||||
title(huge) { "Welcome to blueprint" }
|
||||
text(subtle) { "You can now edit files in the src/ directory" }
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
link(href:index) { button { "Get started" } }
|
||||
link(href:about) { button-light { "About" } }
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
grid(columns:3) {
|
||||
card {
|
||||
title { "Live Reloads" }
|
||||
text { "Changes appear instantly in your browser" }
|
||||
link(href:https://example.com) { button-secondary { "Try Blueprint" } }
|
||||
}
|
||||
card {
|
||||
title { "Modern Design" }
|
||||
text { "Beautiful dark theme with consistent styling" }
|
||||
link(href:about) { button-secondary { "About" } }
|
||||
}
|
||||
card {
|
||||
title { "Easy to Use" }
|
||||
text { "Simple, declarative syntax for building UIs" }
|
||||
link(href:contact) { button-secondary { "Contact" } }
|
||||
}
|
||||
}
|
||||
}
|
BIN
extension/blueprint-language-0.0.1.vsix
Normal file
BIN
extension/blueprint-language-0.0.1.vsix
Normal file
Binary file not shown.
1
extension/client/out/.tsbuildinfo
Normal file
1
extension/client/out/.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
3
extension/client/out/extension.d.ts
vendored
Normal file
3
extension/client/out/extension.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { ExtensionContext } from 'vscode';
|
||||
export declare function activate(context: ExtensionContext): void;
|
||||
export declare function deactivate(): Thenable<void> | undefined;
|
45
extension/client/out/extension.js
Normal file
45
extension/client/out/extension.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
// File: client/src/extension.ts
|
||||
const path = require("path");
|
||||
const vscode_1 = require("vscode");
|
||||
const node_1 = require("vscode-languageclient/node");
|
||||
let client;
|
||||
function activate(context) {
|
||||
// The server is implemented in node
|
||||
const serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const serverOptions = {
|
||||
run: { module: serverModule, transport: node_1.TransportKind.ipc },
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: node_1.TransportKind.ipc,
|
||||
options: debugOptions
|
||||
}
|
||||
};
|
||||
// Options to control the language client
|
||||
const clientOptions = {
|
||||
// Register the server for Blueprint documents
|
||||
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.bp files contained in the workspace
|
||||
fileEvents: vscode_1.workspace.createFileSystemWatcher('**/*.bp')
|
||||
}
|
||||
};
|
||||
// Create and start the client
|
||||
client = new node_1.LanguageClient('blueprintLanguageServer', 'Blueprint Language Server', serverOptions, clientOptions);
|
||||
// Start the client. This will also launch the server
|
||||
client.start();
|
||||
}
|
||||
function deactivate() {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
//# sourceMappingURL=extension.js.map
|
1
extension/client/out/extension.js.map
Normal file
1
extension/client/out/extension.js.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;AAYA,4BAwCC;AAED,gCAKC;AA3DD,gCAAgC;AAChC,6BAA6B;AAC7B,mCAAqD;AACrD,qDAKoC;AAEpC,IAAI,MAAsB,CAAC;AAE3B,SAAgB,QAAQ,CAAC,OAAyB;IAC9C,oCAAoC;IACpC,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,CACvC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,WAAW,CAAC,CAC1C,CAAC;IAEF,mCAAmC;IACnC,MAAM,YAAY,GAAG,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC,EAAE,CAAC;IAElE,oFAAoF;IACpF,qCAAqC;IACrC,MAAM,aAAa,GAAkB;QACjC,GAAG,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,oBAAa,CAAC,GAAG,EAAE;QAC3D,KAAK,EAAE;YACH,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,oBAAa,CAAC,GAAG;YAC5B,OAAO,EAAE,YAAY;SACxB;KACJ,CAAC;IAEF,yCAAyC;IACzC,MAAM,aAAa,GAA0B;QACzC,8CAA8C;QAC9C,gBAAgB,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QAC7D,WAAW,EAAE;YACT,gFAAgF;YAChF,UAAU,EAAE,kBAAS,CAAC,uBAAuB,CAAC,SAAS,CAAC;SAC3D;KACJ,CAAC;IAEF,8BAA8B;IAC9B,MAAM,GAAG,IAAI,qBAAc,CACvB,yBAAyB,EACzB,2BAA2B,EAC3B,aAAa,EACb,aAAa,CAChB,CAAC;IAEF,qDAAqD;IACrD,MAAM,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,SAAgB,UAAU;IACtB,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC"}
|
60
extension/client/src/extension.ts
Normal file
60
extension/client/src/extension.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// File: client/src/extension.ts
|
||||
import * as path from 'path';
|
||||
import { workspace, ExtensionContext } from 'vscode';
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
TransportKind
|
||||
} from 'vscode-languageclient/node';
|
||||
|
||||
let client: LanguageClient;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
// The server is implemented in node
|
||||
const serverModule = context.asAbsolutePath(
|
||||
path.join('server', 'out', 'server.js')
|
||||
);
|
||||
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
|
||||
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { module: serverModule, transport: TransportKind.ipc },
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
options: debugOptions
|
||||
}
|
||||
};
|
||||
|
||||
// Options to control the language client
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for Blueprint documents
|
||||
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.bp files contained in the workspace
|
||||
fileEvents: workspace.createFileSystemWatcher('**/*.bp')
|
||||
}
|
||||
};
|
||||
|
||||
// Create and start the client
|
||||
client = new LanguageClient(
|
||||
'blueprintLanguageServer',
|
||||
'Blueprint Language Server',
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
// Start the client. This will also launch the server
|
||||
client.start();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
16
extension/client/tsconfig.json
Normal file
16
extension/client/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["es2020"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./out/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
77
extension/extension/client/out/extension.js
Normal file
77
extension/extension/client/out/extension.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
// File: client/src/extension.ts
|
||||
const path = __importStar(require("path"));
|
||||
const vscode_1 = require("vscode");
|
||||
const node_1 = require("vscode-languageclient/node");
|
||||
let client;
|
||||
function activate(context) {
|
||||
// The server is implemented in node
|
||||
const serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const serverOptions = {
|
||||
run: { module: serverModule, transport: node_1.TransportKind.ipc },
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: node_1.TransportKind.ipc,
|
||||
options: debugOptions
|
||||
}
|
||||
};
|
||||
// Options to control the language client
|
||||
const clientOptions = {
|
||||
// Register the server for Blueprint documents
|
||||
documentSelector: [{ scheme: 'file', language: 'blueprint' }],
|
||||
synchronize: {
|
||||
// Notify the server about file changes to '.bp files contained in the workspace
|
||||
fileEvents: vscode_1.workspace.createFileSystemWatcher('**/*.bp')
|
||||
}
|
||||
};
|
||||
// Create and start the client
|
||||
client = new node_1.LanguageClient('blueprintLanguageServer', 'Blueprint Language Server', serverOptions, clientOptions);
|
||||
// Start the client. This will also launch the server
|
||||
client.start();
|
||||
}
|
||||
function deactivate() {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
1
extension/extension/tsconfig.tsbuildinfo
Normal file
1
extension/extension/tsconfig.tsbuildinfo
Normal file
|
@ -0,0 +1 @@
|
|||
{"root":["../client/src/extension.ts"],"version":"5.7.2"}
|
1
extension/out/tsconfig.tsbuildinfo
Normal file
1
extension/out/tsconfig.tsbuildinfo
Normal file
|
@ -0,0 +1 @@
|
|||
{"root":["../client/src/extension.ts","../server/src/server.ts"],"version":"5.7.3"}
|
49
extension/package.json
Normal file
49
extension/package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "blueprint-language",
|
||||
"displayName": "Blueprint Language Support",
|
||||
"description": "Language support for Blueprint layout files",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"main": "./client/out/extension.js",
|
||||
"contributes": {
|
||||
"languages": [{
|
||||
"id": "blueprint",
|
||||
"aliases": ["Blueprint", "blueprint"],
|
||||
"extensions": [".bp"],
|
||||
"configuration": "./language-configuration.json"
|
||||
}],
|
||||
"grammars": [{
|
||||
"language": "blueprint",
|
||||
"scopeName": "source.blueprint",
|
||||
"path": "./syntaxes/blueprint.tmLanguage.json"
|
||||
}]
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:blueprint"
|
||||
],
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -b",
|
||||
"watch": "tsc -b -w",
|
||||
"compile:client": "tsc -b ./client/tsconfig.json",
|
||||
"compile:server": "tsc -b ./server/tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^8.1.0",
|
||||
"vscode-languageserver": "^8.1.0",
|
||||
"vscode-languageserver-textdocument": "^1.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/vscode": "^1.75.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.54.0",
|
||||
"eslint": "^8.35.0",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
1
extension/server/out/.tsbuildinfo
Normal file
1
extension/server/out/.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
extension/server/out/server.d.ts
vendored
Normal file
1
extension/server/out/server.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export {};
|
223
extension/server/out/server.js
Normal file
223
extension/server/out/server.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_1 = require("vscode-languageserver/node");
|
||||
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
|
||||
// Create a connection for the server
|
||||
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
||||
// Create a text document manager
|
||||
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
||||
// Blueprint template
|
||||
const blueprintTemplate = `page {
|
||||
title { "$1" }
|
||||
description { "$2" }
|
||||
keywords { "$3" }
|
||||
author { "$4" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:$5) { text(bold) { "$6" } }
|
||||
links {
|
||||
link(href:$7) { "$8" }
|
||||
link(href:$9) { "$10" }
|
||||
link(href:$11) { "$12" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
vertical(centered) {
|
||||
title(huge,margin:0) { "$13" }
|
||||
text(subtle,margin:0) { "$14" }
|
||||
}
|
||||
}`;
|
||||
// Blueprint elements that can be used
|
||||
const elements = [
|
||||
'section', 'grid', 'horizontal', 'vertical', 'title', 'text',
|
||||
'link', 'links', 'button', 'button-light', 'button-secondary', 'button-compact',
|
||||
'card', 'badge', 'alert', 'tooltip', 'input', 'textarea', 'select',
|
||||
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
||||
];
|
||||
// Single instance elements
|
||||
const singleElements = ['page', 'navbar'];
|
||||
// Blueprint properties
|
||||
const properties = [
|
||||
'wide', 'centered', 'alternate', 'padding', 'margin', 'columns', 'responsive',
|
||||
'gap', 'spaced', 'huge', 'large', 'small', 'tiny', 'bold', 'light', 'normal',
|
||||
'italic', 'underline', 'strike', 'uppercase', 'lowercase', 'capitalize',
|
||||
'subtle', 'accent', 'error', 'success', 'warning', 'hover-scale', 'hover-raise',
|
||||
'hover-glow', 'hover-underline', 'hover-fade', 'focus-glow', 'focus-outline',
|
||||
'focus-scale', 'active-scale', 'active-color', 'active-raise', 'mobile-stack',
|
||||
'mobile-hide', 'tablet-wrap', 'tablet-hide', 'desktop-wide', 'desktop-hide'
|
||||
];
|
||||
// Page configuration properties
|
||||
const pageProperties = ['title', 'description', 'keywords', 'author'];
|
||||
// Container elements that can have children
|
||||
const containerElements = [
|
||||
'horizontal', 'vertical', 'section', 'grid', 'navbar',
|
||||
'links', 'card'
|
||||
];
|
||||
connection.onInitialize((params) => {
|
||||
const result = {
|
||||
capabilities: {
|
||||
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
||||
completionProvider: {
|
||||
resolveProvider: false,
|
||||
triggerCharacters: ['{', '(', ' ', '!']
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
});
|
||||
// Check if an element exists in the document
|
||||
function elementExists(text, element) {
|
||||
const regex = new RegExp(`\\b${element}\\s*{`, 'i');
|
||||
return regex.test(text);
|
||||
}
|
||||
// This handler provides the initial list of completion items.
|
||||
connection.onCompletion((textDocumentPosition) => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (!document) {
|
||||
return [];
|
||||
}
|
||||
const text = document.getText();
|
||||
const lines = text.split('\n');
|
||||
const position = textDocumentPosition.position;
|
||||
const line = lines[position.line];
|
||||
const linePrefix = line.slice(0, position.character);
|
||||
// Check if this is a template completion trigger
|
||||
if (linePrefix.trim() === '!') {
|
||||
return [{
|
||||
label: '!blueprint',
|
||||
kind: node_1.CompletionItemKind.Snippet,
|
||||
insertText: blueprintTemplate,
|
||||
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||
documentation: 'Insert Blueprint starter template with customizable placeholders',
|
||||
preselect: true,
|
||||
// Add a command to delete the '!' character
|
||||
additionalTextEdits: [{
|
||||
range: {
|
||||
start: { line: position.line, character: linePrefix.indexOf('!') },
|
||||
end: { line: position.line, character: linePrefix.indexOf('!') + 1 }
|
||||
},
|
||||
newText: ''
|
||||
}]
|
||||
}];
|
||||
}
|
||||
// Inside page block
|
||||
if (text.includes('page {') && !text.includes('}')) {
|
||||
return pageProperties.map(prop => ({
|
||||
label: prop,
|
||||
kind: node_1.CompletionItemKind.Property,
|
||||
insertText: `${prop} { "$1" }`,
|
||||
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||
documentation: `Add ${prop} to the page configuration`
|
||||
}));
|
||||
}
|
||||
// After an opening parenthesis, suggest properties
|
||||
if (linePrefix.trim().endsWith('(')) {
|
||||
return properties.map(prop => ({
|
||||
label: prop,
|
||||
kind: node_1.CompletionItemKind.Property,
|
||||
documentation: `Apply ${prop} property`
|
||||
}));
|
||||
}
|
||||
// After a container element's opening brace, suggest child elements
|
||||
const containerMatch = /\b(horizontal|vertical|section|grid|navbar|links|card)\s*{\s*$/.exec(linePrefix);
|
||||
if (containerMatch) {
|
||||
const parentElement = containerMatch[1];
|
||||
let suggestedElements = elements;
|
||||
// Customize suggestions based on parent element
|
||||
switch (parentElement) {
|
||||
case 'navbar':
|
||||
suggestedElements = ['horizontal', 'vertical', 'link', 'links', 'text'];
|
||||
break;
|
||||
case 'links':
|
||||
suggestedElements = ['link'];
|
||||
break;
|
||||
case 'card':
|
||||
suggestedElements = ['title', 'text', 'button', 'image'];
|
||||
break;
|
||||
}
|
||||
return suggestedElements.map(element => ({
|
||||
label: element,
|
||||
kind: node_1.CompletionItemKind.Class,
|
||||
insertText: `${element} {\n $1\n}`,
|
||||
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||
documentation: `Create a ${element} block inside ${parentElement}`
|
||||
}));
|
||||
}
|
||||
// Get available single instance elements
|
||||
const availableSingleElements = singleElements.filter(element => !elementExists(text, element));
|
||||
// Combine regular elements with available single instance elements
|
||||
const availableElements = [
|
||||
...elements,
|
||||
...availableSingleElements
|
||||
];
|
||||
// Default: suggest elements
|
||||
return availableElements.map(element => {
|
||||
const isPage = element === 'page';
|
||||
const insertText = isPage ?
|
||||
'page {\n title { "$1" }\n description { "$2" }\n keywords { "$3" }\n author { "$4" }\n}' :
|
||||
`${element} {\n $1\n}`;
|
||||
return {
|
||||
label: element,
|
||||
kind: node_1.CompletionItemKind.Class,
|
||||
insertText: insertText,
|
||||
insertTextFormat: node_1.InsertTextFormat.Snippet,
|
||||
documentation: `Create a ${element} block${isPage ? ' (only one allowed per file)' : ''}`
|
||||
};
|
||||
});
|
||||
});
|
||||
// Find all occurrences of an element in the document
|
||||
function findElementOccurrences(text, element) {
|
||||
const occurrences = [];
|
||||
const lines = text.split('\n');
|
||||
const regex = new RegExp(`\\b(${element})\\s*{`, 'g');
|
||||
lines.forEach((line, lineIndex) => {
|
||||
let match;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
const startChar = match.index;
|
||||
const endChar = match.index + match[1].length;
|
||||
occurrences.push({
|
||||
start: { line: lineIndex, character: startChar },
|
||||
end: { line: lineIndex, character: endChar }
|
||||
});
|
||||
}
|
||||
});
|
||||
return occurrences;
|
||||
}
|
||||
// Validate the document for duplicate elements
|
||||
function validateDocument(document) {
|
||||
const text = document.getText();
|
||||
const diagnostics = [];
|
||||
// Check for duplicate single instance elements
|
||||
singleElements.forEach(element => {
|
||||
const occurrences = findElementOccurrences(text, element);
|
||||
if (occurrences.length > 1) {
|
||||
// Add diagnostic for each duplicate occurrence (skip the first one)
|
||||
occurrences.slice(1).forEach(occurrence => {
|
||||
diagnostics.push({
|
||||
severity: node_1.DiagnosticSeverity.Error,
|
||||
range: node_1.Range.create(occurrence.start, occurrence.end),
|
||||
message: `Only one ${element} element is allowed per file.`,
|
||||
source: 'blueprint'
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
// Send the diagnostics to the client
|
||||
connection.sendDiagnostics({ uri: document.uri, diagnostics });
|
||||
}
|
||||
// Set up document validation events
|
||||
documents.onDidChangeContent((change) => {
|
||||
validateDocument(change.document);
|
||||
});
|
||||
documents.onDidOpen((event) => {
|
||||
validateDocument(event.document);
|
||||
});
|
||||
// Make the text document manager listen on the connection
|
||||
documents.listen(connection);
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
//# sourceMappingURL=server.js.map
|
1
extension/server/out/server.js.map
Normal file
1
extension/server/out/server.js.map
Normal file
File diff suppressed because one or more lines are too long
271
extension/server/src/server.ts
Normal file
271
extension/server/src/server.ts
Normal file
|
@ -0,0 +1,271 @@
|
|||
import {
|
||||
createConnection,
|
||||
TextDocuments,
|
||||
ProposedFeatures,
|
||||
InitializeParams,
|
||||
TextDocumentSyncKind,
|
||||
InitializeResult,
|
||||
CompletionItem,
|
||||
CompletionItemKind,
|
||||
TextDocumentPositionParams,
|
||||
InsertTextFormat,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
Position,
|
||||
Range,
|
||||
TextDocumentChangeEvent
|
||||
} from 'vscode-languageserver/node';
|
||||
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument';
|
||||
|
||||
// Create a connection for the server
|
||||
const connection = createConnection(ProposedFeatures.all);
|
||||
|
||||
// Create a text document manager
|
||||
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
|
||||
|
||||
// Blueprint template
|
||||
const blueprintTemplate = `page {
|
||||
title { "$1" }
|
||||
description { "$2" }
|
||||
keywords { "$3" }
|
||||
author { "$4" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:$5) { text(bold) { "$6" } }
|
||||
links {
|
||||
link(href:$7) { "$8" }
|
||||
link(href:$9) { "$10" }
|
||||
link(href:$11) { "$12" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
vertical(centered) {
|
||||
title(huge,margin:0) { "$13" }
|
||||
text(subtle,margin:0) { "$14" }
|
||||
}
|
||||
}`;
|
||||
|
||||
// Blueprint elements that can be used
|
||||
const elements = [
|
||||
'section', 'grid', 'horizontal', 'vertical', 'title', 'text',
|
||||
'link', 'links', 'button', 'button-light', 'button-secondary', 'button-compact',
|
||||
'card', 'badge', 'alert', 'tooltip', 'input', 'textarea', 'select',
|
||||
'checkbox', 'radio', 'switch', 'list', 'table', 'progress', 'slider'
|
||||
];
|
||||
|
||||
// Single instance elements
|
||||
const singleElements = ['page', 'navbar'];
|
||||
|
||||
// Blueprint properties
|
||||
const properties = [
|
||||
'wide', 'centered', 'alternate', 'padding', 'margin', 'columns', 'responsive',
|
||||
'gap', 'spaced', 'huge', 'large', 'small', 'tiny', 'bold', 'light', 'normal',
|
||||
'italic', 'underline', 'strike', 'uppercase', 'lowercase', 'capitalize',
|
||||
'subtle', 'accent', 'error', 'success', 'warning', 'hover-scale', 'hover-raise',
|
||||
'hover-glow', 'hover-underline', 'hover-fade', 'focus-glow', 'focus-outline',
|
||||
'focus-scale', 'active-scale', 'active-color', 'active-raise', 'mobile-stack',
|
||||
'mobile-hide', 'tablet-wrap', 'tablet-hide', 'desktop-wide', 'desktop-hide'
|
||||
];
|
||||
|
||||
// Page configuration properties
|
||||
const pageProperties = ['title', 'description', 'keywords', 'author'];
|
||||
|
||||
// Container elements that can have children
|
||||
const containerElements = [
|
||||
'horizontal', 'vertical', 'section', 'grid', 'navbar',
|
||||
'links', 'card'
|
||||
];
|
||||
|
||||
connection.onInitialize((params: InitializeParams) => {
|
||||
const result: InitializeResult = {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
completionProvider: {
|
||||
resolveProvider: false,
|
||||
triggerCharacters: ['{', '(', ' ', '!']
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
// Check if an element exists in the document
|
||||
function elementExists(text: string, element: string): boolean {
|
||||
const regex = new RegExp(`\\b${element}\\s*{`, 'i');
|
||||
return regex.test(text);
|
||||
}
|
||||
|
||||
// This handler provides the initial list of completion items.
|
||||
connection.onCompletion(
|
||||
(textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (!document) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const text = document.getText();
|
||||
const lines = text.split('\n');
|
||||
const position = textDocumentPosition.position;
|
||||
const line = lines[position.line];
|
||||
const linePrefix = line.slice(0, position.character);
|
||||
|
||||
// Check if this is a template completion trigger
|
||||
if (linePrefix.trim() === '!') {
|
||||
return [{
|
||||
label: '!blueprint',
|
||||
kind: CompletionItemKind.Snippet,
|
||||
insertText: blueprintTemplate,
|
||||
insertTextFormat: InsertTextFormat.Snippet,
|
||||
documentation: 'Insert Blueprint starter template with customizable placeholders',
|
||||
preselect: true,
|
||||
// Add a command to delete the '!' character
|
||||
additionalTextEdits: [{
|
||||
range: {
|
||||
start: { line: position.line, character: linePrefix.indexOf('!') },
|
||||
end: { line: position.line, character: linePrefix.indexOf('!') + 1 }
|
||||
},
|
||||
newText: ''
|
||||
}]
|
||||
}];
|
||||
}
|
||||
|
||||
// Inside page block
|
||||
if (text.includes('page {') && !text.includes('}')) {
|
||||
return pageProperties.map(prop => ({
|
||||
label: prop,
|
||||
kind: CompletionItemKind.Property,
|
||||
insertText: `${prop} { "$1" }`,
|
||||
insertTextFormat: InsertTextFormat.Snippet,
|
||||
documentation: `Add ${prop} to the page configuration`
|
||||
}));
|
||||
}
|
||||
|
||||
// After an opening parenthesis, suggest properties
|
||||
if (linePrefix.trim().endsWith('(')) {
|
||||
return properties.map(prop => ({
|
||||
label: prop,
|
||||
kind: CompletionItemKind.Property,
|
||||
documentation: `Apply ${prop} property`
|
||||
}));
|
||||
}
|
||||
|
||||
// After a container element's opening brace, suggest child elements
|
||||
const containerMatch = /\b(horizontal|vertical|section|grid|navbar|links|card)\s*{\s*$/.exec(linePrefix);
|
||||
if (containerMatch) {
|
||||
const parentElement = containerMatch[1];
|
||||
let suggestedElements = elements;
|
||||
|
||||
// Customize suggestions based on parent element
|
||||
switch (parentElement) {
|
||||
case 'navbar':
|
||||
suggestedElements = ['horizontal', 'vertical', 'link', 'links', 'text'];
|
||||
break;
|
||||
case 'links':
|
||||
suggestedElements = ['link'];
|
||||
break;
|
||||
case 'card':
|
||||
suggestedElements = ['title', 'text', 'button', 'image'];
|
||||
break;
|
||||
}
|
||||
|
||||
return suggestedElements.map(element => ({
|
||||
label: element,
|
||||
kind: CompletionItemKind.Class,
|
||||
insertText: `${element} {\n $1\n}`,
|
||||
insertTextFormat: InsertTextFormat.Snippet,
|
||||
documentation: `Create a ${element} block inside ${parentElement}`
|
||||
}));
|
||||
}
|
||||
|
||||
// Get available single instance elements
|
||||
const availableSingleElements = singleElements.filter(element => !elementExists(text, element));
|
||||
|
||||
// Combine regular elements with available single instance elements
|
||||
const availableElements = [
|
||||
...elements,
|
||||
...availableSingleElements
|
||||
];
|
||||
|
||||
// Default: suggest elements
|
||||
return availableElements.map(element => {
|
||||
const isPage = element === 'page';
|
||||
const insertText = isPage ?
|
||||
'page {\n title { "$1" }\n description { "$2" }\n keywords { "$3" }\n author { "$4" }\n}' :
|
||||
`${element} {\n $1\n}`;
|
||||
|
||||
return {
|
||||
label: element,
|
||||
kind: CompletionItemKind.Class,
|
||||
insertText: insertText,
|
||||
insertTextFormat: InsertTextFormat.Snippet,
|
||||
documentation: `Create a ${element} block${isPage ? ' (only one allowed per file)' : ''}`
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Find all occurrences of an element in the document
|
||||
function findElementOccurrences(text: string, element: string): { start: Position; end: Position; }[] {
|
||||
const occurrences: { start: Position; end: Position; }[] = [];
|
||||
const lines = text.split('\n');
|
||||
const regex = new RegExp(`\\b(${element})\\s*{`, 'g');
|
||||
|
||||
lines.forEach((line, lineIndex) => {
|
||||
let match;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
const startChar = match.index;
|
||||
const endChar = match.index + match[1].length;
|
||||
occurrences.push({
|
||||
start: { line: lineIndex, character: startChar },
|
||||
end: { line: lineIndex, character: endChar }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
// Validate the document for duplicate elements
|
||||
function validateDocument(document: TextDocument): void {
|
||||
const text = document.getText();
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
|
||||
// Check for duplicate single instance elements
|
||||
singleElements.forEach(element => {
|
||||
const occurrences = findElementOccurrences(text, element);
|
||||
if (occurrences.length > 1) {
|
||||
// Add diagnostic for each duplicate occurrence (skip the first one)
|
||||
occurrences.slice(1).forEach(occurrence => {
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: Range.create(occurrence.start, occurrence.end),
|
||||
message: `Only one ${element} element is allowed per file.`,
|
||||
source: 'blueprint'
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send the diagnostics to the client
|
||||
connection.sendDiagnostics({ uri: document.uri, diagnostics });
|
||||
}
|
||||
|
||||
// Set up document validation events
|
||||
documents.onDidChangeContent((change: TextDocumentChangeEvent<TextDocument>) => {
|
||||
validateDocument(change.document);
|
||||
});
|
||||
|
||||
documents.onDidOpen((event: TextDocumentChangeEvent<TextDocument>) => {
|
||||
validateDocument(event.document);
|
||||
});
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
documents.listen(connection);
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
16
extension/server/tsconfig.json
Normal file
16
extension/server/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["es2020"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./out/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
136
extension/syntaxes/blueprint.tmLanguage.json
Normal file
136
extension/syntaxes/blueprint.tmLanguage.json
Normal file
|
@ -0,0 +1,136 @@
|
|||
{
|
||||
"name": "Blueprint",
|
||||
"scopeName": "source.blueprint",
|
||||
"fileTypes": ["bp"],
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#page-config"
|
||||
},
|
||||
{
|
||||
"include": "#elements"
|
||||
},
|
||||
{
|
||||
"include": "#properties"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#punctuation"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"match": "//.*$",
|
||||
"name": "comment.line.double-slash.blueprint"
|
||||
},
|
||||
"page-config": {
|
||||
"begin": "\\b(page)\\b",
|
||||
"end": "(?=})",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "entity.name.tag.blueprint" }
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"begin": "\\b(description|keywords|author)\\b\\s*\\{",
|
||||
"end": "\\}",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "entity.name.tag.blueprint" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"begin": "\\b(title)\\b\\s*\\{",
|
||||
"end": "\\}",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "entity.name.tag.title.blueprint" }
|
||||
},
|
||||
"patterns": [
|
||||
{ "include": "#strings" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"elements": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(section|grid|horizontal|vertical|navbar|title|text|link|links|button|button-light|button-secondary|button-compact|card|badge|alert|tooltip|input|textarea|select|checkbox|radio|switch|list|table|progress|slider|media)\\b",
|
||||
"name": "entity.name.tag.blueprint"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(wide|centered|alternate|padding|margin|columns|responsive|gap|spaced|huge|large|small|tiny|bold|light|normal|italic|underline|strike|uppercase|lowercase|capitalize|subtle|accent|error|success|warning|hover-scale|hover-raise|hover-glow|hover-underline|hover-fade|focus-glow|focus-outline|focus-scale|active-scale|active-color|active-raise|mobile-stack|mobile-hide|tablet-wrap|tablet-hide|desktop-wide|desktop-hide)\\b",
|
||||
"name": "support.type.property-name.blueprint"
|
||||
},
|
||||
{
|
||||
"match": "(?<!:)(src|type|href|\\w+)\\s*:",
|
||||
"captures": {
|
||||
"1": { "name": "support.type.property-name.blueprint" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(?<=type:)\\s*(img|video)\\b",
|
||||
"name": "string.other.media-type.blueprint"
|
||||
},
|
||||
{
|
||||
"match": "(?<=src:|href:)\\s*https?:\\/\\/[\\w\\-\\.]+(?:\\/[\\w\\-\\.\\/?=&%]*)?",
|
||||
"name": "string.url.blueprint"
|
||||
},
|
||||
{
|
||||
"match": "#[0-9a-fA-F]{3,6}",
|
||||
"name": "constant.other.color.hex.blueprint"
|
||||
},
|
||||
{
|
||||
"match": "\\b\\d+(%|px|rem|em)?\\b",
|
||||
"name": "constant.numeric.blueprint"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"name": "string.quoted.double.blueprint",
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\\\.",
|
||||
"name": "constant.character.escape.blueprint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"name": "string.quoted.single.blueprint",
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\\\.",
|
||||
"name": "constant.character.escape.blueprint"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "[{}()]",
|
||||
"name": "punctuation.section.blueprint"
|
||||
},
|
||||
{
|
||||
"match": ",",
|
||||
"name": "punctuation.separator.blueprint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
26
extension/tsconfig.json
Normal file
26
extension/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "out",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["es2020"],
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"client/src",
|
||||
"server/src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./client" },
|
||||
{ "path": "./server" }
|
||||
]
|
||||
}
|
343
lib/ASTBuilder.js
Normal file
343
lib/ASTBuilder.js
Normal file
|
@ -0,0 +1,343 @@
|
|||
const BlueprintError = require("./BlueprintError");
|
||||
const { ELEMENT_MAPPINGS } = require("./mappings");
|
||||
|
||||
class ASTBuilder {
|
||||
/**
|
||||
* Initializes a new instance of the ASTBuilder class.
|
||||
*
|
||||
* @param {Object} options - Configuration options for the ASTBuilder.
|
||||
* @param {boolean} [options.debug=false] - If true, enables debug logging for the builder.
|
||||
*/
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[ASTBuilder] Initialized with options:",
|
||||
JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a node object into a JSON string representation.
|
||||
* Handles circular references by replacing them with a predefined string.
|
||||
*
|
||||
* @param {Object} node - The node object to stringify.
|
||||
* @returns {string} - The JSON string representation of the node.
|
||||
* If unable to stringify, returns an error message.
|
||||
*/
|
||||
|
||||
debugStringify(node) {
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (key === "parent") return "[Circular:Parent]";
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(node, getCircularReplacer(), 2);
|
||||
} catch (err) {
|
||||
return `[Unable to stringify: ${err.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an Abstract Syntax Tree (AST) from a sequence of tokens.
|
||||
*
|
||||
* This function iterates over the provided tokens to build a hierarchical
|
||||
* AST structure. It identifies elements, their properties, and any nested
|
||||
* child elements, converting them into structured nodes. Each node is
|
||||
* represented as an object containing type, tag, properties, children,
|
||||
* and position information (line and column).
|
||||
*
|
||||
* Throws an error if unexpected tokens are encountered or if there are
|
||||
* mismatched braces.
|
||||
*
|
||||
* @param {Array} tokens - The list of tokens to be parsed into an AST.
|
||||
* @returns {Object} - The constructed AST root node with its children.
|
||||
* Each child represents either an element or text node.
|
||||
* @throws {BlueprintError} - If unexpected tokens or structural issues are found.
|
||||
*/
|
||||
|
||||
buildAST(tokens) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[ASTBuilder] Starting AST construction");
|
||||
console.log(`[ASTBuilder] Processing ${tokens.length} tokens`);
|
||||
console.log(
|
||||
"[ASTBuilder] First few tokens:",
|
||||
tokens
|
||||
.slice(0, 3)
|
||||
.map((t) => this.debugStringify(t))
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
let current = 0;
|
||||
|
||||
/**
|
||||
* Walks the token list to construct a hierarchical AST structure.
|
||||
*
|
||||
* This function is responsible for processing each token and constructing
|
||||
* the corresponding node in the AST. It handles elements, their properties,
|
||||
* and any nested child elements, converting them into structured nodes.
|
||||
* Each node is represented as an object containing type, tag, properties,
|
||||
* children, and position information (line and column).
|
||||
*
|
||||
* Throws an error if unexpected tokens are encountered or if there are
|
||||
* mismatched braces.
|
||||
*
|
||||
* @returns {Object} - The constructed AST node with its children.
|
||||
* Each child represents either an element or text node.
|
||||
* @throws {BlueprintError} - If unexpected tokens or structural issues are found.
|
||||
*/
|
||||
const walk = () => {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[ASTBuilder] Walking tokens at position ${current}/${tokens.length}`
|
||||
);
|
||||
console.log(
|
||||
"[ASTBuilder] Current token:",
|
||||
this.debugStringify(tokens[current])
|
||||
);
|
||||
}
|
||||
|
||||
let token = tokens[current];
|
||||
|
||||
if (!token) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[ASTBuilder] Unexpected end of input while walking tokens"
|
||||
);
|
||||
console.log(
|
||||
"[ASTBuilder] Last processed token:",
|
||||
this.debugStringify(tokens[current - 1])
|
||||
);
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"Unexpected end of input",
|
||||
tokens[tokens.length - 1]?.line || 1,
|
||||
tokens[tokens.length - 1]?.column || 0
|
||||
);
|
||||
}
|
||||
|
||||
if (token.type === "identifier") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[ASTBuilder] Processing identifier: "${token.value}" at line ${token.line}, column ${token.column}`
|
||||
);
|
||||
}
|
||||
|
||||
const elementType = token.value;
|
||||
if (!ELEMENT_MAPPINGS[elementType]) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Error: Unknown element type "${elementType}"`
|
||||
);
|
||||
console.log(
|
||||
"[ASTBuilder] Available element types:",
|
||||
Object.keys(ELEMENT_MAPPINGS).join(", ")
|
||||
);
|
||||
}
|
||||
throw new BlueprintError(
|
||||
`Unknown element type: ${elementType}`,
|
||||
token.line,
|
||||
token.column
|
||||
);
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS[elementType];
|
||||
const node = {
|
||||
type: "element",
|
||||
tag: elementType,
|
||||
props:
|
||||
elementType === "page" ? [] : [...(mapping.defaultProps || [])],
|
||||
children: [],
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
};
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[ASTBuilder] Created node for element "${elementType}"`);
|
||||
console.log(
|
||||
"[ASTBuilder] Initial node state:",
|
||||
this.debugStringify(node)
|
||||
);
|
||||
}
|
||||
|
||||
current++;
|
||||
|
||||
if (
|
||||
current < tokens.length &&
|
||||
tokens[current].type === "props"
|
||||
) {
|
||||
const props = tokens[current].value.split(",").map((p) => p.trim());
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Processing ${props.length} properties for "${elementType}"`
|
||||
);
|
||||
console.log("[ASTBuilder] Properties:", props);
|
||||
}
|
||||
|
||||
props.forEach((prop) => {
|
||||
const [name, ...valueParts] = prop.split(":");
|
||||
const value = valueParts.join(":").trim();
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Processing property - name: "${name}", value: "${value}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
if (elementType === "page") {
|
||||
const processedProp = {
|
||||
name,
|
||||
value: value.replace(/^"|"$/g, ""),
|
||||
};
|
||||
node.props.push(processedProp);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Added page property:`,
|
||||
processedProp
|
||||
);
|
||||
}
|
||||
} else {
|
||||
node.props.push(`${name}:${value}`);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Added property: "${name}:${value}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node.props.push(name);
|
||||
if (this.options.debug) {
|
||||
console.log(`[ASTBuilder] Added flag property: "${name}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
current++;
|
||||
}
|
||||
|
||||
if (
|
||||
current < tokens.length &&
|
||||
tokens[current].type === "brace" &&
|
||||
tokens[current].value === "{"
|
||||
) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[ASTBuilder] Processing child elements for "${elementType}"`
|
||||
);
|
||||
}
|
||||
current++;
|
||||
|
||||
while (
|
||||
current < tokens.length &&
|
||||
!(tokens[current].type === "brace" && tokens[current].value === "}")
|
||||
) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Processing child at position ${current}`
|
||||
);
|
||||
}
|
||||
const child = walk();
|
||||
child.parent = node;
|
||||
node.children.push(child);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Added child to "${elementType}":`,
|
||||
this.debugStringify(child)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (current >= tokens.length) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Error: Missing closing brace for "${elementType}"`
|
||||
);
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"Missing closing brace",
|
||||
node.line,
|
||||
node.column
|
||||
);
|
||||
}
|
||||
|
||||
current++;
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[ASTBuilder] Completed node for "${elementType}"`);
|
||||
console.log(
|
||||
"[ASTBuilder] Final node state:",
|
||||
this.debugStringify(node)
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
if (token.type === "text") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[ASTBuilder] Processing text node at line ${token.line}, column ${token.column}`
|
||||
);
|
||||
console.log(`[ASTBuilder] Text content: "${token.value}"`);
|
||||
}
|
||||
current++;
|
||||
return {
|
||||
type: "text",
|
||||
value: token.value,
|
||||
line: token.line,
|
||||
column: token.column,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[ASTBuilder] Error: Unexpected token type: ${token.type}`);
|
||||
console.log("[ASTBuilder] Token details:", this.debugStringify(token));
|
||||
}
|
||||
throw new BlueprintError(
|
||||
`Unexpected token type: ${token.type}`,
|
||||
token.line,
|
||||
token.column
|
||||
);
|
||||
};
|
||||
|
||||
const ast = {
|
||||
type: "root",
|
||||
children: [],
|
||||
};
|
||||
|
||||
while (current < tokens.length) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[ASTBuilder] Processing root-level token at position ${current}`
|
||||
);
|
||||
}
|
||||
ast.children.push(walk());
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[ASTBuilder] AST construction complete");
|
||||
console.log(`[ASTBuilder] Total nodes: ${ast.children.length}`);
|
||||
console.log(
|
||||
"[ASTBuilder] Root children types:",
|
||||
ast.children.map((c) => c.type).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ASTBuilder;
|
144
lib/BlueprintBuilder.js
Normal file
144
lib/BlueprintBuilder.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TokenParser = require("./TokenParser");
|
||||
const ASTBuilder = require("./ASTBuilder");
|
||||
const CSSGenerator = require("./CSSGenerator");
|
||||
const HTMLGenerator = require("./HTMLGenerator");
|
||||
const MetadataManager = require("./MetadataManager");
|
||||
|
||||
class BlueprintBuilder {
|
||||
/**
|
||||
* Create a new Blueprint builder instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.minified=true] - Minify generated HTML and CSS
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
minified: true,
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.tokenParser = new TokenParser(this.options);
|
||||
this.astBuilder = new ASTBuilder(this.options);
|
||||
this.cssGenerator = new CSSGenerator(this.options);
|
||||
this.htmlGenerator = new HTMLGenerator(this.options, this.cssGenerator);
|
||||
this.metadataManager = new MetadataManager(this.options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a Blueprint file.
|
||||
* @param {string} inputPath - Path to the Blueprint file to build
|
||||
* @param {string} outputDir - Directory to write the generated HTML and CSS files
|
||||
* @returns {Object} - Build result object with `success` and `errors` properties
|
||||
*/
|
||||
build(inputPath, outputDir) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Starting build for ${inputPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!inputPath.endsWith(".bp")) {
|
||||
throw new Error("Input file must have .bp extension");
|
||||
}
|
||||
|
||||
const input = fs.readFileSync(inputPath, "utf8");
|
||||
|
||||
const tokens = this.tokenParser.tokenize(input);
|
||||
|
||||
const ast = this.astBuilder.buildAST(tokens);
|
||||
|
||||
const pageNode = ast.children.find((node) => node.tag === "page");
|
||||
if (pageNode) {
|
||||
this.metadataManager.processPageMetadata(pageNode);
|
||||
}
|
||||
|
||||
const html = this.htmlGenerator.generateHTML(ast);
|
||||
const css = this.cssGenerator.generateCSS();
|
||||
|
||||
const baseName = path.basename(inputPath, ".bp");
|
||||
const headContent = this.metadataManager.generateHeadContent(baseName);
|
||||
const finalHtml = this.generateFinalHtml(headContent, html);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, `${baseName}.html`), finalHtml);
|
||||
fs.writeFileSync(path.join(outputDir, `${baseName}.css`), css);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Build completed successfully");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Build failed with error:", error);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
message: error.message,
|
||||
type: error.name,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the final HTML document as a string.
|
||||
*
|
||||
* @param {string} headContent - The HTML content to be placed within the <head> tag.
|
||||
* @param {string} bodyContent - The HTML content to be placed within the <body> tag.
|
||||
* @returns {string} - A complete HTML document containing the provided head and body content.
|
||||
*/
|
||||
|
||||
generateFinalHtml(headContent, bodyContent) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
${headContent}
|
||||
<style>
|
||||
:root {
|
||||
--navbar-height: 4rem;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: var(--navbar-height);
|
||||
background-color: #0d1117;
|
||||
color: #e6edf3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
::selection {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintBuilder;
|
10
lib/BlueprintError.js
Normal file
10
lib/BlueprintError.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
class BlueprintError extends Error {
|
||||
constructor(message, line = null, column = null) {
|
||||
super(message);
|
||||
this.name = "BlueprintError";
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintError;
|
402
lib/CSSGenerator.js
Normal file
402
lib/CSSGenerator.js
Normal file
|
@ -0,0 +1,402 @@
|
|||
const { STYLE_MAPPINGS } = require("./mappings");
|
||||
|
||||
class CSSGenerator {
|
||||
/**
|
||||
* Creates a new CSS generator instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.minified=true] - Minify generated class names
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.cssRules = new Map();
|
||||
this.classCounter = 0;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[CSSGenerator] Initialized with options:",
|
||||
JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a class name for the given element type, based on the counter
|
||||
* and the minified option. If minified is true, the class name will be a
|
||||
* single lowercase letter (a-z), or a single uppercase letter (A-Z) if
|
||||
* the counter is between 26 and 51. Otherwise, it will be a complex
|
||||
* class name (e.g. "zabcdefg") with a counter starting from 52.
|
||||
*
|
||||
* @param {string} elementType - The type of the element for which to
|
||||
* generate a class name.
|
||||
* @return {string} The generated class name.
|
||||
*/
|
||||
generateClassName(elementType) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[CSSGenerator] Generating class name for element type: "${elementType}"`
|
||||
);
|
||||
console.log(`[CSSGenerator] Current class counter: ${this.classCounter}`);
|
||||
}
|
||||
|
||||
let className;
|
||||
if (!this.options.minified) {
|
||||
className = `blueprint-${elementType}-${this.classCounter++}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generated readable class name: "${className}"`
|
||||
);
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
if (this.classCounter < 26) {
|
||||
className = String.fromCharCode(97 + this.classCounter++);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generated lowercase class name: "${className}" (counter: ${
|
||||
this.classCounter - 1
|
||||
})`
|
||||
);
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
if (this.classCounter < 52) {
|
||||
className = String.fromCharCode(65 + (this.classCounter++ - 26));
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generated uppercase class name: "${className}" (counter: ${
|
||||
this.classCounter - 1
|
||||
})`
|
||||
);
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const base = chars.length;
|
||||
let num = this.classCounter++ - 52;
|
||||
let result = "";
|
||||
|
||||
do {
|
||||
result = chars[num % base] + result;
|
||||
num = Math.floor(num / base);
|
||||
} while (num > 0);
|
||||
|
||||
result = "z" + result;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generated complex class name: "${result}" (counter: ${
|
||||
this.classCounter - 1
|
||||
})`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a node to CSS properties, using the style mappings to process
|
||||
* the node's properties. The generated CSS properties are returned as a
|
||||
* Map, where each key is a CSS property name and each value is the value
|
||||
* for that property.
|
||||
*
|
||||
* @param {Object} node - The node to convert
|
||||
* @return {Object} - The generated CSS properties and nested rules
|
||||
*/
|
||||
nodeToCSSProperties(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[CSSGenerator] Converting node to CSS properties`);
|
||||
console.log(`[CSSGenerator] Node tag: "${node.tag}"`);
|
||||
console.log("[CSSGenerator] Node properties:", node.props);
|
||||
}
|
||||
|
||||
const cssProps = new Map();
|
||||
const nestedRules = new Map();
|
||||
|
||||
node.props.forEach((prop) => {
|
||||
if (typeof prop === "object") {
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Skipping object property:`, prop);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [name, value] = prop.split(/[-:]/);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[CSSGenerator] Processing property - name: "${name}", value: "${value}"`
|
||||
);
|
||||
}
|
||||
// This is for customization of css properties
|
||||
|
||||
if (name === "width" && !isNaN(value)) {
|
||||
cssProps.set("width", `${value}% !important`);
|
||||
cssProps.set("max-width", "none !important");
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set width: ${value}% !important and max-width: none !important`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "height" && !isNaN(value)) {
|
||||
cssProps.set("height", `${value}% !important`);
|
||||
cssProps.set("max-height", "none !important");
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set height: ${value}% !important and max-height: none !important`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "padding" && !isNaN(value)) {
|
||||
cssProps.set("padding", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set padding: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "margin" && !isNaN(value)) {
|
||||
cssProps.set("margin", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set margin: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "marginTop" && !isNaN(value)) {
|
||||
cssProps.set("margin-top", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set margin-top: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "marginBottom" && !isNaN(value)) {
|
||||
cssProps.set("margin-bottom", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set margin-bottom: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "marginLeft" && !isNaN(value)) {
|
||||
cssProps.set("margin-left", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set margin-left: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "marginRight" && !isNaN(value)) {
|
||||
cssProps.set("margin-right", `${value}px !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set margin-right: ${value}px !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "color") {
|
||||
cssProps.set("color", `${value} !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set color: ${value} !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "backgroundColor") {
|
||||
cssProps.set("background-color", `${value} !important`);
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Set background-color: ${value} !important`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const style = STYLE_MAPPINGS[name];
|
||||
if (style) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[CSSGenerator] Processing style mapping for: "${name}"`);
|
||||
}
|
||||
Object.entries(style).forEach(([key, baseValue]) => {
|
||||
if (typeof baseValue === "object") {
|
||||
if (key.startsWith(":") || key.startsWith(">")) {
|
||||
nestedRules.set(key, baseValue);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Added nested rule: "${key}" =>`,
|
||||
baseValue
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let finalValue = baseValue;
|
||||
if (value && key === "gridTemplateColumns" && !isNaN(value)) {
|
||||
finalValue = `repeat(${value}, 1fr)`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set grid template columns: ${finalValue}`
|
||||
);
|
||||
}
|
||||
}
|
||||
cssProps.set(key, finalValue);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set CSS property: "${key}" = "${finalValue}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let finalValue = baseValue;
|
||||
if (value && key === "gridTemplateColumns" && !isNaN(value)) {
|
||||
finalValue = `repeat(${value}, 1fr)`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set grid template columns: ${finalValue}`
|
||||
);
|
||||
}
|
||||
}
|
||||
cssProps.set(key, finalValue);
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Set CSS property: "${key}" = "${finalValue}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[CSSGenerator] CSS properties generation complete");
|
||||
console.log(`[CSSGenerator] Generated ${cssProps.size} CSS properties`);
|
||||
console.log(`[CSSGenerator] Generated ${nestedRules.size} nested rules`);
|
||||
}
|
||||
|
||||
return { cssProps, nestedRules };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the CSS code for the given style mappings. If minified is true,
|
||||
* the generated CSS will be minified. Otherwise, it will be formatted with
|
||||
* indentation and newlines.
|
||||
*
|
||||
* @return {string} The generated CSS code
|
||||
*/
|
||||
generateCSS() {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[CSSGenerator] Starting CSS generation");
|
||||
console.log(`[CSSGenerator] Processing ${this.cssRules.size} rule sets`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camelCase string to kebab-case (lowercase with hyphens
|
||||
* separating words)
|
||||
*
|
||||
* @param {string} str The string to convert
|
||||
* @return {string} The converted string
|
||||
*/
|
||||
|
||||
const toKebabCase = (str) =>
|
||||
str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
|
||||
let css = "";
|
||||
this.cssRules.forEach((props, selector) => {
|
||||
if (props.cssProps.size > 0) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[CSSGenerator] Generating CSS for selector: "${selector}"`
|
||||
);
|
||||
console.log(
|
||||
`[CSSGenerator] Properties count: ${props.cssProps.size}`
|
||||
);
|
||||
}
|
||||
css += `${selector} {${this.options.minified ? "" : "\n"}`;
|
||||
props.cssProps.forEach((value, prop) => {
|
||||
const cssProperty = toKebabCase(prop);
|
||||
css += `${
|
||||
this.options.minified ? "" : " "
|
||||
}${cssProperty}: ${value};${this.options.minified ? "" : "\n"}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Added property: ${cssProperty}: ${value}`
|
||||
);
|
||||
}
|
||||
});
|
||||
css += `}${this.options.minified ? "" : "\n"}`;
|
||||
}
|
||||
|
||||
if (props.nestedRules.size > 0) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[CSSGenerator] Processing ${props.nestedRules.size} nested rules for "${selector}"`
|
||||
);
|
||||
}
|
||||
props.nestedRules.forEach((rules, nestedSelector) => {
|
||||
const fullSelector = nestedSelector.startsWith(">")
|
||||
? `${selector} ${nestedSelector}`
|
||||
: `${selector}${nestedSelector}`;
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generating nested selector: "${fullSelector}"`
|
||||
);
|
||||
}
|
||||
|
||||
css += `${fullSelector} {${this.options.minified ? "" : "\n"}`;
|
||||
Object.entries(rules).forEach(([prop, value]) => {
|
||||
if (typeof value === "object") {
|
||||
const pseudoSelector = `${fullSelector}${prop}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Generating pseudo-selector: "${pseudoSelector}"`
|
||||
);
|
||||
}
|
||||
css += `}${this.options.minified ? "" : "\n"}${pseudoSelector} {${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
Object.entries(value).forEach(([nestedProp, nestedValue]) => {
|
||||
const cssProperty = toKebabCase(nestedProp);
|
||||
css += `${
|
||||
this.options.minified ? "" : " "
|
||||
}${cssProperty}: ${nestedValue};${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Added nested property: ${cssProperty}: ${nestedValue}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const cssProperty = toKebabCase(prop);
|
||||
css += `${
|
||||
this.options.minified ? "" : " "
|
||||
}${cssProperty}: ${value};${this.options.minified ? "" : "\n"}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[CSSGenerator] Added property: ${cssProperty}: ${value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
css += `}${this.options.minified ? "" : "\n"}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[CSSGenerator] CSS generation complete");
|
||||
console.log(
|
||||
`[CSSGenerator] Generated ${css.split("\n").length} lines of CSS`
|
||||
);
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CSSGenerator;
|
353
lib/HTMLGenerator.js
Normal file
353
lib/HTMLGenerator.js
Normal file
|
@ -0,0 +1,353 @@
|
|||
const { ELEMENT_MAPPINGS } = require("./mappings");
|
||||
|
||||
class HTMLGenerator {
|
||||
/**
|
||||
* Creates a new HTML generator instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.minified=true] - Minify generated HTML
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
*/
|
||||
constructor(options = {}, cssGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[HTMLGenerator] Initialized with options:",
|
||||
JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a node to a string for debugging purposes, avoiding circular
|
||||
* references.
|
||||
* @param {Object} node - Node to stringify
|
||||
* @returns {string} String representation of the node
|
||||
*/
|
||||
debugStringify(node) {
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (key === "parent") return "[Circular:Parent]";
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(node, getCircularReplacer(), 2);
|
||||
} catch (err) {
|
||||
return `[Unable to stringify: ${err.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a node to a string of HTML.
|
||||
* @param {Object} node - Node to generate HTML for
|
||||
* @returns {string} Generated HTML
|
||||
*/
|
||||
generateHTML(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[HTMLGenerator] Generating HTML for node`);
|
||||
console.log(`[HTMLGenerator] Node type: "${node.type}"`);
|
||||
console.log("[HTMLGenerator] Node details:", this.debugStringify(node));
|
||||
}
|
||||
|
||||
if (node.type === "text") {
|
||||
if (node.parent?.tag === "codeblock") {
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Rendering raw text for codeblock");
|
||||
console.log(`[HTMLGenerator] Raw text content: "${node.value}"`);
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
const escapedText = node.value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Generated escaped text");
|
||||
console.log(`[HTMLGenerator] Original: "${node.value}"`);
|
||||
console.log(`[HTMLGenerator] Escaped: "${escapedText}"`);
|
||||
}
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
let html = "";
|
||||
if (node.type === "element") {
|
||||
if (node.tag === "page") {
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Skipping page node - metadata only");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS[node.tag];
|
||||
let tag = mapping ? mapping.tag : "div";
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } =
|
||||
this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[HTMLGenerator] Processing element node`);
|
||||
console.log(`[HTMLGenerator] Tag: "${node.tag}" -> "${tag}"`);
|
||||
console.log(`[HTMLGenerator] Generated class name: "${className}"`);
|
||||
console.log(
|
||||
"[HTMLGenerator] CSS properties:",
|
||||
this.debugStringify(Object.fromEntries(cssProps))
|
||||
);
|
||||
console.log(
|
||||
"[HTMLGenerator] Nested rules:",
|
||||
this.debugStringify(Object.fromEntries(nestedRules))
|
||||
);
|
||||
}
|
||||
|
||||
let attributes = "";
|
||||
if (tag === "input") {
|
||||
if (node.tag === "checkbox") {
|
||||
attributes = ' type="checkbox"';
|
||||
} else if (node.tag === "radio") {
|
||||
attributes = ' type="radio"';
|
||||
} else if (node.tag === "switch") {
|
||||
attributes = ' type="checkbox" role="switch"';
|
||||
} else if (node.tag === "slider") {
|
||||
attributes = ' type="range"';
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added input attributes: "${attributes}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tag === "media") {
|
||||
const srcProp = node.props.find((p) => p.startsWith("src:"));
|
||||
const typeProp = node.props.find((p) => p.startsWith("type:"));
|
||||
|
||||
if (!srcProp) {
|
||||
throw new BlueprintError("Media element requires src property", node.line, node.column);
|
||||
}
|
||||
|
||||
const src = srcProp.substring(srcProp.indexOf(":") + 1).trim();
|
||||
const type = typeProp ? typeProp.substring(typeProp.indexOf(":") + 1).trim() : "img";
|
||||
|
||||
if (type === "video") {
|
||||
tag = "video";
|
||||
attributes = ` src="${src}" controls`;
|
||||
} else {
|
||||
tag = "img";
|
||||
attributes = ` src="${src}" alt="${node.children.map(child => this.generateHTML(child)).join("")}"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tag === "link") {
|
||||
const linkInfo = this.processLink(node);
|
||||
attributes += ` href="${linkInfo.href}"`;
|
||||
if (
|
||||
linkInfo.href.startsWith("http://") ||
|
||||
linkInfo.href.startsWith("https://")
|
||||
) {
|
||||
attributes += ` target="_blank" rel="noopener noreferrer"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added external link attributes for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added internal link attributes for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
node.props.find((p) => typeof p === "string" && p.startsWith("data-"))
|
||||
) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
);
|
||||
attributes += " " + dataProps.map((p) => `${p}`).join(" ");
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added data attributes:`,
|
||||
this.debugStringify(dataProps)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Registered CSS rules for class: .${className}`
|
||||
);
|
||||
}
|
||||
|
||||
if (node.tag === "button" || node.tag.startsWith("button-")) {
|
||||
if (node.parent?.tag === "link") {
|
||||
const linkInfo = this.processLink(node.parent);
|
||||
if (
|
||||
linkInfo.href.startsWith("http://") ||
|
||||
linkInfo.href.startsWith("https://")
|
||||
) {
|
||||
attributes += ` onclick="window.open('${linkInfo.href}', '_blank', 'noopener,noreferrer')"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added external button click handler for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
attributes += ` onclick="window.location.href='${linkInfo.href}'"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added internal button click handler for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
html += `<button class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Generated button opening tag with attributes:`,
|
||||
this.debugStringify({ class: className, ...attributes })
|
||||
);
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
html += `${this.options.minified ? "" : "\n"}</button>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
} else if (
|
||||
node.tag === "link" &&
|
||||
node.children.length === 1 &&
|
||||
(node.children[0].tag === "button" ||
|
||||
node.children[0].tag?.startsWith("button-"))
|
||||
) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[HTMLGenerator] Processing button inside link - using button's HTML"
|
||||
);
|
||||
}
|
||||
node.children[0].parent = node;
|
||||
html += this.generateHTML(node.children[0]);
|
||||
} else {
|
||||
html += `<${tag} class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Generated opening tag: <${tag}> with attributes:`,
|
||||
this.debugStringify({ class: className, ...attributes })
|
||||
);
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
html += `${this.options.minified ? "" : "\n"}</${tag}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Completed element: ${tag}`);
|
||||
}
|
||||
}
|
||||
} else if (node.type === "root") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Processing root node with ${node.children.length} children`
|
||||
);
|
||||
}
|
||||
node.children.forEach((child, index) => {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Processing root child ${index + 1}/${
|
||||
node.children.length
|
||||
}`
|
||||
);
|
||||
}
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Generated HTML:", html);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a link node, extracting the href attribute and converting it
|
||||
* to an internal link if it doesn't start with http:// or https://.
|
||||
*
|
||||
* If no href property is found, the default value of # is used.
|
||||
*
|
||||
* @param {Object} node - The link node to process
|
||||
* @returns {Object} - An object containing the final href value
|
||||
*/
|
||||
processLink(node) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[HTMLGenerator] Processing link node");
|
||||
console.log(
|
||||
"[HTMLGenerator] Link properties:",
|
||||
this.debugStringify(node.props)
|
||||
);
|
||||
}
|
||||
|
||||
const hrefProp = node.props.find((p) => p.startsWith("href:"));
|
||||
let href = "#";
|
||||
|
||||
if (hrefProp) {
|
||||
let hrefTarget = hrefProp
|
||||
.substring(hrefProp.indexOf(":") + 1)
|
||||
.trim()
|
||||
.replace(/^"|"$/g, "");
|
||||
if (
|
||||
!hrefTarget.startsWith("http://") &&
|
||||
!hrefTarget.startsWith("https://")
|
||||
) {
|
||||
hrefTarget = "/" + hrefTarget;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Converted to internal link: "${hrefTarget}"`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] External link detected: "${hrefTarget}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
href = hrefTarget;
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[HTMLGenerator] No href property found, using default: '#'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Final href value: "${href}"`);
|
||||
}
|
||||
return { href };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HTMLGenerator;
|
333
lib/MetadataManager.js
Normal file
333
lib/MetadataManager.js
Normal file
|
@ -0,0 +1,333 @@
|
|||
class MetadataManager {
|
||||
/**
|
||||
* Initializes a new instance of the MetadataManager class.
|
||||
*
|
||||
* @param {Object} options - Configuration options for the metadata manager.
|
||||
* @param {boolean} [options.debug=false] - Enables debug logging if true.
|
||||
*
|
||||
* Sets up the pageMetadata object containing default title, faviconUrl, and an empty meta array.
|
||||
* If debug mode is enabled, logs the initialization options and the initial metadata state.
|
||||
*/
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.pageMetadata = {
|
||||
title: "",
|
||||
faviconUrl: "",
|
||||
meta: [],
|
||||
};
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[MetadataManager] Initialized with options:",
|
||||
JSON.stringify(options, null, 2)
|
||||
);
|
||||
console.log(
|
||||
"[MetadataManager] Initial metadata state:",
|
||||
JSON.stringify(this.pageMetadata, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a node to a string for debugging purposes, avoiding circular
|
||||
* references.
|
||||
* @param {Object} node - Node to stringify
|
||||
* @returns {string} String representation of the node
|
||||
*/
|
||||
debugStringify(node) {
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (key === "parent") return "[Circular:Parent]";
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(node, getCircularReplacer(), 2);
|
||||
} catch (err) {
|
||||
return `[Unable to stringify: ${err.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the metadata of a given node object, updating the internal page metadata state.
|
||||
*
|
||||
* Iterates through the node's properties and children to extract metadata information such as
|
||||
* title, favicon, description, keywords, and author. This information is used to populate
|
||||
* the pageMetadata object.
|
||||
*
|
||||
* For each property or child, it handles known metadata fields directly and adds custom
|
||||
* meta tags for any properties or children with a "meta-" prefix.
|
||||
*
|
||||
* @param {Object} node - The node containing properties and children to process for metadata.
|
||||
*/
|
||||
|
||||
processPageMetadata(node) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[MetadataManager] Processing page metadata");
|
||||
console.log("[MetadataManager] Node details:", this.debugStringify(node));
|
||||
}
|
||||
|
||||
if (node.props) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[MetadataManager] Processing ${node.props.length} page properties`
|
||||
);
|
||||
console.log(
|
||||
"[MetadataManager] Properties:",
|
||||
this.debugStringify(node.props)
|
||||
);
|
||||
}
|
||||
node.props.forEach((prop) => {
|
||||
if (typeof prop === "object" && prop.name && prop.value) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[MetadataManager] Processing property:`,
|
||||
this.debugStringify(prop)
|
||||
);
|
||||
}
|
||||
switch (prop.name) {
|
||||
case "title":
|
||||
this.pageMetadata.title = prop.value;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Set page title: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "favicon":
|
||||
this.pageMetadata.faviconUrl = prop.value;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Set favicon URL: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
this.pageMetadata.meta.push({
|
||||
name: "description",
|
||||
content: prop.value,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added description meta tag: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "keywords":
|
||||
this.pageMetadata.meta.push({
|
||||
name: "keywords",
|
||||
content: prop.value,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added keywords meta tag: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "author":
|
||||
this.pageMetadata.meta.push({
|
||||
name: "author",
|
||||
content: prop.value,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added author meta tag: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (prop.name.startsWith("meta-")) {
|
||||
const metaName = prop.name.substring(5);
|
||||
this.pageMetadata.meta.push({
|
||||
name: metaName,
|
||||
content: prop.value,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added custom meta tag - ${metaName}: "${prop.value}"`
|
||||
);
|
||||
}
|
||||
} else if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Skipping unknown property: "${prop.name}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[MetadataManager] Processing ${node.children.length} child nodes for metadata`
|
||||
);
|
||||
}
|
||||
node.children.forEach((child, index) => {
|
||||
if (child.tag) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`\n[MetadataManager] Processing child ${index + 1}/${
|
||||
node.children.length
|
||||
}`
|
||||
);
|
||||
console.log(`[MetadataManager] Child tag: "${child.tag}"`);
|
||||
console.log(
|
||||
"[MetadataManager] Child details:",
|
||||
this.debugStringify(child)
|
||||
);
|
||||
}
|
||||
|
||||
let content = "";
|
||||
/**
|
||||
* Recursively extracts the text content from a node tree.
|
||||
*
|
||||
* This function traverses the node tree and concatenates the text content of
|
||||
* all text nodes. For non-text nodes, it recursively calls itself on the
|
||||
* children of that node.
|
||||
*
|
||||
* @param {Object} node - The node for which to extract the text content
|
||||
* @return {string} The extracted text content
|
||||
*/
|
||||
const getTextContent = (node) => {
|
||||
if (node.type === "text") return node.value;
|
||||
if (node.children) {
|
||||
return node.children.map(getTextContent).join("");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
content = getTextContent(child);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`[MetadataManager] Extracted content: "${content}"`);
|
||||
}
|
||||
|
||||
switch (child.tag) {
|
||||
case "title":
|
||||
this.pageMetadata.title = content;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Set page title from child: "${content}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
this.pageMetadata.meta.push({ name: "description", content });
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added description meta tag from child: "${content}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "keywords":
|
||||
this.pageMetadata.meta.push({ name: "keywords", content });
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added keywords meta tag from child: "${content}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "author":
|
||||
this.pageMetadata.meta.push({ name: "author", content });
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added author meta tag from child: "${content}"`
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (child.tag.startsWith("meta-")) {
|
||||
const metaName = child.tag.substring(5);
|
||||
this.pageMetadata.meta.push({ name: metaName, content });
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added custom meta tag from child - ${metaName}: "${content}"`
|
||||
);
|
||||
}
|
||||
} else if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Skipping unknown child tag: "${child.tag}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[MetadataManager] Metadata processing complete");
|
||||
console.log(
|
||||
"[MetadataManager] Final metadata state:",
|
||||
this.debugStringify(this.pageMetadata)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML head content for the page, based on the metadata
|
||||
* previously collected. The generated content includes the page title,
|
||||
* favicon link, meta tags, and stylesheet link.
|
||||
*
|
||||
* @param {string} baseName - The base name of the page (used for the
|
||||
* stylesheet link)
|
||||
* @return {string} The generated HTML head content
|
||||
*/
|
||||
generateHeadContent(baseName) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[MetadataManager] Generating head content");
|
||||
console.log(`[MetadataManager] Base name: "${baseName}"`);
|
||||
}
|
||||
|
||||
let content = "";
|
||||
|
||||
const title = this.pageMetadata.title || baseName;
|
||||
content += ` <title>${title}</title>\n`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[MetadataManager] Added title tag: "${title}"`);
|
||||
}
|
||||
|
||||
if (this.pageMetadata.faviconUrl) {
|
||||
content += ` <link rel="icon" href="${this.pageMetadata.faviconUrl}">\n`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added favicon link: "${this.pageMetadata.faviconUrl}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Processing ${this.pageMetadata.meta.length} meta tags`
|
||||
);
|
||||
}
|
||||
this.pageMetadata.meta.forEach((meta, index) => {
|
||||
content += ` <meta name="${meta.name}" content="${meta.content}">\n`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[MetadataManager] Added meta tag ${index + 1}: ${meta.name} = "${
|
||||
meta.content
|
||||
}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
content += ` <link rel="stylesheet" href="${baseName}.css">\n`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[MetadataManager] Added stylesheet link: "${baseName}.css"`);
|
||||
console.log("\n[MetadataManager] Head content generation complete");
|
||||
console.log("[MetadataManager] Generated content:", content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetadataManager;
|
370
lib/TokenParser.js
Normal file
370
lib/TokenParser.js
Normal file
|
@ -0,0 +1,370 @@
|
|||
const BlueprintError = require("./BlueprintError");
|
||||
|
||||
class TokenParser {
|
||||
|
||||
/**
|
||||
* Creates a new TokenParser instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[TokenParser] Initialized with options:",
|
||||
JSON.stringify(options, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes the input string into an array of tokens.
|
||||
* Tokens can be of the following types:
|
||||
* - `identifier`: A sequence of letters, numbers, underscores, and hyphens.
|
||||
* Represents a CSS selector or a property name.
|
||||
* - `props`: A sequence of characters enclosed in parentheses.
|
||||
* Represents a list of CSS properties.
|
||||
* - `text`: A sequence of characters enclosed in quotes.
|
||||
* Represents a string of text.
|
||||
* - `brace`: A single character, either `{` or `}`.
|
||||
* Represents a brace in the input.
|
||||
*
|
||||
* @param {string} input - Input string to tokenize
|
||||
* @returns {Array<Object>} - Array of tokens
|
||||
* @throws {BlueprintError} - If the input contains invalid syntax
|
||||
*/
|
||||
tokenize(input) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[TokenParser] Starting tokenization");
|
||||
console.log(`[TokenParser] Input length: ${input.length} characters`);
|
||||
console.log(`[TokenParser] First 100 chars: ${input.slice(0, 100)}...`);
|
||||
}
|
||||
|
||||
const tokens = [];
|
||||
let current = 0;
|
||||
let line = 1;
|
||||
let column = 1;
|
||||
const startTime = Date.now();
|
||||
const TIMEOUT_MS = 5000;
|
||||
|
||||
while (current < input.length) {
|
||||
let char = input[current];
|
||||
|
||||
if (Date.now() - startTime > TIMEOUT_MS) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Tokenization timeout at position ${current}, line ${line}, column ${column}`
|
||||
);
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"Parsing timeout - check for unclosed brackets or quotes",
|
||||
line,
|
||||
column
|
||||
);
|
||||
}
|
||||
|
||||
if (char === "\n") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Line break at position ${current}, moving to line ${
|
||||
line + 1
|
||||
}`
|
||||
);
|
||||
}
|
||||
line++;
|
||||
column = 1;
|
||||
current++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\s/.test(char)) {
|
||||
column++;
|
||||
current++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && input[current + 1] === "/") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Comment found at line ${line}, column ${column}`
|
||||
);
|
||||
const commentEnd = input.indexOf("\n", current);
|
||||
const comment = input.slice(
|
||||
current,
|
||||
commentEnd !== -1 ? commentEnd : undefined
|
||||
);
|
||||
console.log(`[TokenParser] Comment content: ${comment}`);
|
||||
}
|
||||
while (current < input.length && input[current] !== "\n") {
|
||||
current++;
|
||||
column++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/[a-zA-Z]/.test(char)) {
|
||||
let value = "";
|
||||
const startColumn = column;
|
||||
const startPos = current;
|
||||
|
||||
while (current < input.length && /[a-zA-Z0-9_-]/.test(char)) {
|
||||
value += char;
|
||||
current++;
|
||||
column++;
|
||||
char = input[current];
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Identifier found at line ${line}, column ${startColumn}`
|
||||
);
|
||||
console.log(`[TokenParser] Identifier value: "${value}"`);
|
||||
console.log(
|
||||
`[TokenParser] Context: ...${input.slice(
|
||||
Math.max(0, startPos - 10),
|
||||
startPos
|
||||
)}[${value}]${input.slice(current, current + 10)}...`
|
||||
);
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: "identifier",
|
||||
value,
|
||||
line,
|
||||
column: startColumn,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "(") {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Starting property list at position ${current}`);
|
||||
}
|
||||
|
||||
const startColumn = column;
|
||||
let value = "";
|
||||
let depth = 1;
|
||||
let propLine = line;
|
||||
let propColumn = column;
|
||||
current++;
|
||||
column++;
|
||||
const propStartPos = current;
|
||||
|
||||
while (current < input.length && depth > 0) {
|
||||
if (current - propStartPos > 1000) {
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Property list too long or unclosed");
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"Property list too long or unclosed parenthesis",
|
||||
propLine,
|
||||
propColumn
|
||||
);
|
||||
}
|
||||
|
||||
char = input[current];
|
||||
|
||||
if (char === "(") depth++;
|
||||
if (char === ")") depth--;
|
||||
|
||||
if (depth === 0) break;
|
||||
|
||||
value += char;
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Unclosed parenthesis detected");
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"Unclosed parenthesis in property list",
|
||||
propLine,
|
||||
propColumn
|
||||
);
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: "props",
|
||||
value: value.trim(),
|
||||
line,
|
||||
column: startColumn,
|
||||
});
|
||||
|
||||
current++;
|
||||
column++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Starting string at position ${current}`);
|
||||
}
|
||||
|
||||
const startColumn = column;
|
||||
const startLine = line;
|
||||
const quote = char;
|
||||
let value = "";
|
||||
const stringStartPos = current;
|
||||
|
||||
current++;
|
||||
column++;
|
||||
|
||||
while (current < input.length) {
|
||||
if (current - stringStartPos > 1000) {
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] String too long or unclosed");
|
||||
}
|
||||
throw new BlueprintError(
|
||||
"String too long or unclosed quote",
|
||||
startLine,
|
||||
startColumn
|
||||
);
|
||||
}
|
||||
|
||||
char = input[current];
|
||||
|
||||
if (char === "\n") {
|
||||
line++;
|
||||
column = 1;
|
||||
value += char;
|
||||
} else if (char === quote && input[current - 1] !== "\\") {
|
||||
break;
|
||||
} else {
|
||||
value += char;
|
||||
column++;
|
||||
}
|
||||
|
||||
current++;
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: "text",
|
||||
value,
|
||||
line: startLine,
|
||||
column: startColumn,
|
||||
});
|
||||
|
||||
current++;
|
||||
column++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "{" || char === "}") {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Found brace: ${char} at position ${current}`);
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: "brace",
|
||||
value: char,
|
||||
line,
|
||||
column,
|
||||
});
|
||||
current++;
|
||||
column++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[DEBUG] Unexpected character at position ${current}: "${char}"`
|
||||
);
|
||||
}
|
||||
throw new BlueprintError(`Unexpected character: ${char}`, line, column);
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[TokenParser] Tokenization complete");
|
||||
console.log(`[TokenParser] Total tokens generated: ${tokens.length}`);
|
||||
console.log(
|
||||
"[TokenParser] Token summary:",
|
||||
tokens.map((t) => `${t.type}:${t.value}`).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
this.validateBraces(tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that all braces in the token stream are properly matched.
|
||||
* This function walks the token stream, counting the number of open and
|
||||
* close braces. If it encounters an unmatched brace, it throws an error.
|
||||
* If it encounters an extra closing brace, it throws an error.
|
||||
* @throws {BlueprintError} - If there is a brace mismatch
|
||||
*/
|
||||
validateBraces(tokens) {
|
||||
let braceCount = 0;
|
||||
let lastOpenBrace = { line: 1, column: 1 };
|
||||
const braceStack = [];
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("\n[TokenParser] Starting brace validation");
|
||||
}
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.type === "brace") {
|
||||
if (token.value === "{") {
|
||||
braceCount++;
|
||||
braceStack.push({ line: token.line, column: token.column });
|
||||
lastOpenBrace = { line: token.line, column: token.column };
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Opening brace at line ${token.line}, column ${token.column}, depth: ${braceCount}`
|
||||
);
|
||||
}
|
||||
} else if (token.value === "}") {
|
||||
braceCount--;
|
||||
const matchingOpen = braceStack.pop();
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Closing brace at line ${token.line}, column ${token.column}, depth: ${braceCount}`
|
||||
);
|
||||
if (matchingOpen) {
|
||||
console.log(
|
||||
`[TokenParser] Matches opening brace at line ${matchingOpen.line}, column ${matchingOpen.column}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (braceCount !== 0) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[TokenParser] Brace mismatch detected: ${
|
||||
braceCount > 0 ? "unclosed" : "extra"
|
||||
} braces`
|
||||
);
|
||||
console.log(`[TokenParser] Brace stack:`, braceStack);
|
||||
}
|
||||
if (braceCount > 0) {
|
||||
throw new BlueprintError(
|
||||
"Unclosed brace",
|
||||
lastOpenBrace.line,
|
||||
lastOpenBrace.column
|
||||
);
|
||||
} else {
|
||||
throw new BlueprintError(
|
||||
"Extra closing brace",
|
||||
tokens[tokens.length - 1].line,
|
||||
tokens[tokens.length - 1].column
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[TokenParser] Brace validation complete - all braces match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TokenParser;
|
77
lib/build.js
Normal file
77
lib/build.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const BlueprintBuilder = require("./BlueprintBuilder");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
minified: !args.includes("--readable"),
|
||||
srcDir: "./src",
|
||||
outDir: "./dist",
|
||||
debug: args.includes("--debug"),
|
||||
};
|
||||
|
||||
const builder = new BlueprintBuilder(options);
|
||||
|
||||
function ensureDirectoryExistence(filePath) {
|
||||
const dirname = path.dirname(filePath);
|
||||
if (fs.existsSync(dirname)) {
|
||||
return true;
|
||||
}
|
||||
ensureDirectoryExistence(dirname);
|
||||
fs.mkdirSync(dirname);
|
||||
}
|
||||
|
||||
function getAllFiles(dirPath, arrayOfFiles) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
arrayOfFiles = arrayOfFiles || [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (fs.statSync(path.join(dirPath, file)).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(path.join(dirPath, file), arrayOfFiles);
|
||||
} else if (file.endsWith(".bp")) {
|
||||
arrayOfFiles.push(path.join(dirPath, file));
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
|
||||
const files = getAllFiles(options.srcDir);
|
||||
|
||||
let success = true;
|
||||
const errors = [];
|
||||
|
||||
console.log("Building Blueprint files...");
|
||||
const startTime = Date.now();
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(options.srcDir, file);
|
||||
const outputPath = path.join(
|
||||
options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
ensureDirectoryExistence(outputPath);
|
||||
|
||||
console.log(`Building ${file}...`);
|
||||
const result = builder.build(file, path.dirname(outputPath));
|
||||
if (!result.success) {
|
||||
success = false;
|
||||
errors.push({ file, errors: result.errors });
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
if (success) {
|
||||
console.log(`All files built successfully in ${totalTime}ms!`);
|
||||
} else {
|
||||
console.error("Build failed with errors:");
|
||||
errors.forEach(({ file, errors }) => {
|
||||
console.error(`\nFile: ${file}`);
|
||||
errors.forEach((err) => {
|
||||
console.error(` ${err.message} (${err.line}:${err.column})`);
|
||||
});
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
15
lib/dev-server.js
Normal file
15
lib/dev-server.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const BlueprintServer = require("./server");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
port: args.includes("--port")
|
||||
? parseInt(args[args.indexOf("--port") + 1])
|
||||
: 3000,
|
||||
liveReload: args.includes("--live"),
|
||||
minified: !args.includes("--readable"),
|
||||
srcDir: "./src",
|
||||
outDir: "./dist",
|
||||
};
|
||||
|
||||
const server = new BlueprintServer(options);
|
||||
server.start();
|
648
lib/mappings.js
Normal file
648
lib/mappings.js
Normal file
|
@ -0,0 +1,648 @@
|
|||
const STYLE_MAPPINGS = {
|
||||
centered: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
padding: "2rem",
|
||||
width: "100%",
|
||||
},
|
||||
spaced: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: "1.5rem",
|
||||
width: "100%",
|
||||
},
|
||||
responsive: {
|
||||
flexWrap: "wrap",
|
||||
gap: "2rem",
|
||||
},
|
||||
horizontal: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "1.5rem",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
},
|
||||
vertical: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1.5rem",
|
||||
width: "100%",
|
||||
},
|
||||
grid: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||
gap: "2rem",
|
||||
width: "100%",
|
||||
padding: "2rem 0",
|
||||
},
|
||||
wide: {
|
||||
width: "100%",
|
||||
maxWidth: "1200px",
|
||||
margin: "0 auto",
|
||||
padding: "0 2rem",
|
||||
},
|
||||
alternate: {
|
||||
backgroundColor: "#0d1117",
|
||||
padding: "5rem 0",
|
||||
width: "100%",
|
||||
},
|
||||
sticky: {
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
zIndex: "1000",
|
||||
backgroundColor: "rgba(13, 17, 23, 0.95)",
|
||||
backdropFilter: "blur(12px)",
|
||||
borderBottom: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
},
|
||||
|
||||
huge: {
|
||||
fontSize: "clamp(2.5rem, 5vw, 4rem)",
|
||||
fontWeight: "800",
|
||||
lineHeight: "1.1",
|
||||
letterSpacing: "-0.02em",
|
||||
color: "#ffffff",
|
||||
marginBottom: "1.5rem",
|
||||
textAlign: "center",
|
||||
},
|
||||
large: {
|
||||
fontSize: "clamp(1.5rem, 3vw, 2rem)",
|
||||
lineHeight: "1.3",
|
||||
color: "#ffffff",
|
||||
fontWeight: "600",
|
||||
marginBottom: "1rem",
|
||||
},
|
||||
small: {
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1.5",
|
||||
color: "#8b949e",
|
||||
},
|
||||
bold: {
|
||||
fontWeight: "600",
|
||||
color: "#ffffff",
|
||||
},
|
||||
subtle: {
|
||||
color: "#8b949e",
|
||||
lineHeight: "1.6",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
|
||||
light: {
|
||||
backgroundColor: "transparent",
|
||||
color: "#8b949e",
|
||||
padding: "0.875rem 1.75rem",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
cursor: "pointer",
|
||||
fontWeight: "500",
|
||||
fontSize: "0.95rem",
|
||||
transition: "all 0.2s ease",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textDecoration: "none",
|
||||
":hover": {
|
||||
color: "#e6edf3",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
borderColor: "#6b7280",
|
||||
},
|
||||
},
|
||||
|
||||
raised: {
|
||||
backgroundColor: "#111827",
|
||||
borderRadius: "16px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
padding: "2rem",
|
||||
transition: "all 0.2s ease",
|
||||
":hover": {
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "0 8px 16px rgba(0,0,0,0.2)",
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
},
|
||||
prominent: {
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "#ffffff",
|
||||
padding: "0.875rem 1.75rem",
|
||||
borderRadius: "12px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontWeight: "500",
|
||||
fontSize: "0.95rem",
|
||||
transition: "all 0.2s ease",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textDecoration: "none",
|
||||
":hover": {
|
||||
backgroundColor: "#2563eb",
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: "0 4px 12px rgba(59, 130, 246, 0.3)",
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: "#1f2937",
|
||||
color: "#e6edf3",
|
||||
padding: "0.875rem 1.75rem",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
cursor: "pointer",
|
||||
fontWeight: "500",
|
||||
fontSize: "0.95rem",
|
||||
transition: "all 0.2s ease",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textDecoration: "none",
|
||||
":hover": {
|
||||
backgroundColor: "#374151",
|
||||
borderColor: "#6b7280",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
},
|
||||
compact: {
|
||||
padding: "0.75rem",
|
||||
borderRadius: "12px",
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
color: "#e6edf3",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
":hover": {
|
||||
backgroundColor: "#1f2937",
|
||||
borderColor: "#3b82f6",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
},
|
||||
|
||||
navbar: {
|
||||
backgroundColor: "rgba(13, 17, 23, 0.95)",
|
||||
backdropFilter: "blur(12px)",
|
||||
padding: "1rem 2rem",
|
||||
width: "100%",
|
||||
borderBottom: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
"> *": {
|
||||
maxWidth: "1200px",
|
||||
margin: "0 auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
},
|
||||
section: {
|
||||
padding: "5rem 0",
|
||||
backgroundColor: "#0d1117",
|
||||
marginTop: "5rem",
|
||||
"> *": {
|
||||
maxWidth: "1200px",
|
||||
margin: "0 auto",
|
||||
},
|
||||
},
|
||||
card: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1.5rem",
|
||||
height: "100%",
|
||||
backgroundColor: "#111827",
|
||||
borderRadius: "16px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
padding: "2rem",
|
||||
transition: "all 0.2s ease",
|
||||
marginBottom: "1rem",
|
||||
"> title": {
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: "600",
|
||||
color: "#ffffff",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
"> text": {
|
||||
color: "#8b949e",
|
||||
lineHeight: "1.6",
|
||||
},
|
||||
cursor: "default",
|
||||
},
|
||||
links: {
|
||||
display: "flex",
|
||||
gap: "2rem",
|
||||
alignItems: "center",
|
||||
"> *": {
|
||||
color: "#8b949e",
|
||||
textDecoration: "none",
|
||||
transition: "all 0.2s ease",
|
||||
fontSize: "0.95rem",
|
||||
padding: "0.5rem 0.75rem",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
color: "#e6edf3",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
input: {
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
borderRadius: "12px",
|
||||
padding: "0.875rem 1.25rem",
|
||||
color: "#e6edf3",
|
||||
width: "100%",
|
||||
transition: "all 0.2s ease",
|
||||
outline: "none",
|
||||
fontSize: "0.95rem",
|
||||
":focus": {
|
||||
borderColor: "#3b82f6",
|
||||
boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.15)",
|
||||
},
|
||||
"::placeholder": {
|
||||
color: "#8b949e",
|
||||
},
|
||||
},
|
||||
textarea: {
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
borderRadius: "12px",
|
||||
padding: "0.875rem 1.25rem",
|
||||
color: "#e6edf3",
|
||||
width: "100%",
|
||||
minHeight: "120px",
|
||||
resize: "vertical",
|
||||
transition: "all 0.2s ease",
|
||||
outline: "none",
|
||||
fontSize: "0.95rem",
|
||||
":focus": {
|
||||
borderColor: "#3b82f6",
|
||||
boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.15)",
|
||||
},
|
||||
},
|
||||
select: {
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
borderRadius: "12px",
|
||||
padding: "0.875rem 2.5rem 0.875rem 1.25rem",
|
||||
color: "#e6edf3",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
appearance: "none",
|
||||
fontSize: "0.95rem",
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%238b949e' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\")",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "right 1rem center",
|
||||
backgroundSize: "1.5em 1.5em",
|
||||
transition: "all 0.2s ease",
|
||||
":focus": {
|
||||
borderColor: "#3b82f6",
|
||||
boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.15)",
|
||||
},
|
||||
},
|
||||
checkbox: {
|
||||
appearance: "none",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
backgroundColor: "#111827",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
position: "relative",
|
||||
marginRight: "0.75rem",
|
||||
":checked": {
|
||||
backgroundColor: "#3b82f6",
|
||||
borderColor: "#3b82f6",
|
||||
"::after": {
|
||||
content: '"✓"',
|
||||
position: "absolute",
|
||||
color: "white",
|
||||
fontSize: "0.85rem",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
},
|
||||
":hover": {
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
},
|
||||
radio: {
|
||||
appearance: "none",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "50%",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
backgroundColor: "#111827",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
marginRight: "0.75rem",
|
||||
":checked": {
|
||||
borderColor: "#3b82f6",
|
||||
borderWidth: "4px",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
":hover": {
|
||||
borderColor: "#3b82f6",
|
||||
},
|
||||
},
|
||||
progress: {
|
||||
appearance: "none",
|
||||
width: "100%",
|
||||
height: "0.75rem",
|
||||
borderRadius: "999px",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: "#111827",
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: "#3b82f6",
|
||||
transition: "width 0.3s ease",
|
||||
},
|
||||
"::-moz-progress-bar": {
|
||||
backgroundColor: "#3b82f6",
|
||||
transition: "width 0.3s ease",
|
||||
},
|
||||
},
|
||||
slider: {
|
||||
appearance: "none",
|
||||
width: "100%",
|
||||
height: "0.5rem",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
cursor: "pointer",
|
||||
"::-webkit-slider-thumb": {
|
||||
appearance: "none",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#3b82f6",
|
||||
border: "2px solid #ffffff",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
":hover": {
|
||||
transform: "scale(1.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
switch: {
|
||||
appearance: "none",
|
||||
position: "relative",
|
||||
width: "3.5rem",
|
||||
height: "1.75rem",
|
||||
backgroundColor: "#111827",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
borderRadius: "999px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
marginRight: "0.75rem",
|
||||
":checked": {
|
||||
backgroundColor: "#3b82f6",
|
||||
borderColor: "#3b82f6",
|
||||
"::after": {
|
||||
transform: "translateX(1.75rem)",
|
||||
},
|
||||
},
|
||||
"::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "0.2rem",
|
||||
left: "0.2rem",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#ffffff",
|
||||
transition: "transform 0.2s ease",
|
||||
},
|
||||
},
|
||||
badge: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0.375rem 0.875rem",
|
||||
borderRadius: "999px",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "500",
|
||||
backgroundColor: "#111827",
|
||||
color: "#e6edf3",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
minWidth: "4rem",
|
||||
transition: "all 0.2s ease",
|
||||
},
|
||||
alert: {
|
||||
padding: "1rem 1.5rem",
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
backgroundColor: "#111827",
|
||||
color: "#e6edf3",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
fontSize: "0.95rem",
|
||||
},
|
||||
tooltip: {
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
":hover::after": {
|
||||
content: "attr(data-tooltip)",
|
||||
position: "absolute",
|
||||
bottom: "120%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
padding: "0.5rem 1rem",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#111827",
|
||||
color: "#e6edf3",
|
||||
fontSize: "0.875rem",
|
||||
whiteSpace: "nowrap",
|
||||
zIndex: "1000",
|
||||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
||||
border: "1px solid rgba(48, 54, 61, 0.6)",
|
||||
},
|
||||
},
|
||||
link: {
|
||||
color: "#e6edf3",
|
||||
textDecoration: "none",
|
||||
transition: "all 0.2s ease",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
":hover": {
|
||||
color: "#3b82f6",
|
||||
},
|
||||
},
|
||||
media: {
|
||||
display: "block",
|
||||
maxWidth: "100%",
|
||||
height: "auto",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease",
|
||||
":hover": {
|
||||
transform: "scale(1.01)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ELEMENT_MAPPINGS = {
|
||||
page: {
|
||||
tag: "meta",
|
||||
defaultProps: [],
|
||||
},
|
||||
section: {
|
||||
tag: "section",
|
||||
defaultProps: ["wide"],
|
||||
},
|
||||
title: {
|
||||
tag: "h1",
|
||||
defaultProps: ["bold"],
|
||||
},
|
||||
subtitle: {
|
||||
tag: "h2",
|
||||
defaultProps: ["bold", "large"],
|
||||
},
|
||||
text: {
|
||||
tag: "p",
|
||||
defaultProps: [],
|
||||
},
|
||||
button: {
|
||||
tag: "button",
|
||||
defaultProps: ["prominent"],
|
||||
},
|
||||
"button-secondary": {
|
||||
tag: "button",
|
||||
defaultProps: ["secondary"],
|
||||
},
|
||||
"button-light": {
|
||||
tag: "button",
|
||||
defaultProps: ["light"],
|
||||
},
|
||||
"button-compact": {
|
||||
tag: "button",
|
||||
defaultProps: ["compact"],
|
||||
},
|
||||
link: {
|
||||
tag: "a",
|
||||
defaultProps: ["link"],
|
||||
},
|
||||
card: {
|
||||
tag: "div",
|
||||
defaultProps: ["raised", "card"],
|
||||
},
|
||||
grid: {
|
||||
tag: "div",
|
||||
defaultProps: ["grid", "responsive"],
|
||||
},
|
||||
horizontal: {
|
||||
tag: "div",
|
||||
defaultProps: ["horizontal", "spaced"],
|
||||
},
|
||||
vertical: {
|
||||
tag: "div",
|
||||
defaultProps: ["vertical"],
|
||||
},
|
||||
list: {
|
||||
tag: "ul",
|
||||
defaultProps: ["bullet"],
|
||||
},
|
||||
cell: {
|
||||
tag: "td",
|
||||
defaultProps: [],
|
||||
},
|
||||
row: {
|
||||
tag: "tr",
|
||||
defaultProps: [],
|
||||
},
|
||||
table: {
|
||||
tag: "table",
|
||||
defaultProps: ["table"],
|
||||
},
|
||||
codeblock: {
|
||||
tag: "pre",
|
||||
defaultProps: ["code"],
|
||||
},
|
||||
navbar: {
|
||||
tag: "nav",
|
||||
defaultProps: ["navbar", "sticky"],
|
||||
},
|
||||
links: {
|
||||
tag: "div",
|
||||
defaultProps: ["links"],
|
||||
},
|
||||
input: {
|
||||
tag: "input",
|
||||
defaultProps: ["input"],
|
||||
},
|
||||
textarea: {
|
||||
tag: "textarea",
|
||||
defaultProps: ["textarea"],
|
||||
},
|
||||
checkbox: {
|
||||
tag: "input",
|
||||
defaultProps: ["checkbox"],
|
||||
},
|
||||
radio: {
|
||||
tag: "input",
|
||||
defaultProps: ["radio"],
|
||||
},
|
||||
select: {
|
||||
tag: "select",
|
||||
defaultProps: ["select"],
|
||||
},
|
||||
progress: {
|
||||
tag: "progress",
|
||||
defaultProps: ["progress"],
|
||||
},
|
||||
slider: {
|
||||
tag: "input",
|
||||
defaultProps: ["slider"],
|
||||
},
|
||||
switch: {
|
||||
tag: "input",
|
||||
defaultProps: ["switch"],
|
||||
},
|
||||
badge: {
|
||||
tag: "span",
|
||||
defaultProps: ["badge"],
|
||||
},
|
||||
alert: {
|
||||
tag: "div",
|
||||
defaultProps: ["alert"],
|
||||
},
|
||||
tooltip: {
|
||||
tag: "span",
|
||||
defaultProps: ["tooltip"],
|
||||
},
|
||||
description: {
|
||||
tag: "meta",
|
||||
defaultProps: [],
|
||||
},
|
||||
keywords: {
|
||||
tag: "meta",
|
||||
defaultProps: [],
|
||||
},
|
||||
author: {
|
||||
tag: "meta",
|
||||
defaultProps: [],
|
||||
},
|
||||
media: {
|
||||
tag: "media",
|
||||
defaultProps: ["media"],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
STYLE_MAPPINGS,
|
||||
ELEMENT_MAPPINGS,
|
||||
};
|
473
lib/server.js
Normal file
473
lib/server.js
Normal file
|
@ -0,0 +1,473 @@
|
|||
const express = require("express");
|
||||
const expressWs = require("express-ws");
|
||||
const chokidar = require("chokidar");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const BlueprintBuilder = require("./BlueprintBuilder");
|
||||
|
||||
class BlueprintServer {
|
||||
constructor(options = {}) {
|
||||
this.app = express();
|
||||
this.wsInstance = expressWs(this.app);
|
||||
this.options = {
|
||||
port: 3000,
|
||||
srcDir: "./src",
|
||||
outDir: "./dist",
|
||||
liveReload: false,
|
||||
minified: true,
|
||||
...options,
|
||||
};
|
||||
this.clients = new Map();
|
||||
this.filesWithErrors = new Set();
|
||||
this.setupServer();
|
||||
if (this.options.liveReload) {
|
||||
const watcher = chokidar.watch([], {
|
||||
ignored: /(^|[\/\\])\../,
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
watcher.add(this.options.srcDir);
|
||||
this.setupWatcher(watcher);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
log(tag, message, color) {
|
||||
const colorCodes = {
|
||||
blue: "\x1b[34m",
|
||||
green: "\x1b[32m",
|
||||
red: "\x1b[31m",
|
||||
orange: "\x1b[33m",
|
||||
lightGray: "\x1b[90m",
|
||||
reset: "\x1b[0m",
|
||||
bgBlue: "\x1b[44m",
|
||||
};
|
||||
console.log(
|
||||
`${colorCodes.bgBlue} BP ${colorCodes.reset} ${
|
||||
colorCodes[color] || ""
|
||||
}${message}${colorCodes.reset}`
|
||||
);
|
||||
}
|
||||
|
||||
async buildAll() {
|
||||
this.log("INFO", "Building all Blueprint files...", "lightGray");
|
||||
if (fs.existsSync(this.options.outDir)) {
|
||||
fs.rmSync(this.options.outDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(this.options.outDir, { recursive: true });
|
||||
|
||||
const files = this.getAllFiles(this.options.srcDir);
|
||||
let success = true;
|
||||
const errors = [];
|
||||
const startTime = Date.now();
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(this.options.srcDir, file);
|
||||
const outputPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
|
||||
const builder = new BlueprintBuilder({ minified: this.options.minified });
|
||||
const result = builder.build(file, path.dirname(outputPath));
|
||||
if (!result.success) {
|
||||
success = false;
|
||||
errors.push({ file, errors: result.errors });
|
||||
}
|
||||
}
|
||||
const totalTime = Date.now() - startTime;
|
||||
if (success) {
|
||||
this.log(
|
||||
"SUCCESS",
|
||||
`All files built successfully in ${totalTime}ms!`,
|
||||
"green"
|
||||
);
|
||||
} else {
|
||||
this.log("ERROR", "Build failed with errors:", "red");
|
||||
errors.forEach(({ file, errors }) => {
|
||||
this.log("ERROR", `File: ${file}`, "red");
|
||||
errors.forEach((err) => {
|
||||
this.log(
|
||||
"ERROR",
|
||||
`${err.type} at line ${err.line}, column ${err.column}: ${err.message}`,
|
||||
"red"
|
||||
);
|
||||
});
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ensureDirectoryExistence(filePath) {
|
||||
const dirname = path.dirname(filePath);
|
||||
if (fs.existsSync(dirname)) {
|
||||
return true;
|
||||
}
|
||||
this.ensureDirectoryExistence(dirname);
|
||||
fs.mkdirSync(dirname);
|
||||
}
|
||||
|
||||
getAllFiles(dirPath, arrayOfFiles) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
arrayOfFiles = arrayOfFiles || [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (fs.statSync(path.join(dirPath, file)).isDirectory()) {
|
||||
arrayOfFiles = this.getAllFiles(path.join(dirPath, file), arrayOfFiles);
|
||||
} else if (file.endsWith(".bp")) {
|
||||
arrayOfFiles.push(path.join(dirPath, file));
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
this.app.use((req, res, next) => {
|
||||
const isHtmlRequest =
|
||||
req.path.endsWith(".html") || !path.extname(req.path);
|
||||
|
||||
if (this.options.liveReload && isHtmlRequest) {
|
||||
const htmlPath = req.path.endsWith(".html")
|
||||
? path.join(this.options.outDir, req.path)
|
||||
: path.join(this.options.outDir, req.path + ".html");
|
||||
|
||||
fs.readFile(htmlPath, "utf8", (err, data) => {
|
||||
if (err) return next();
|
||||
let html = data;
|
||||
const script = `
|
||||
<script>
|
||||
(function() {
|
||||
let currentPage = window.location.pathname.replace(/^\\//, '') || 'index.html';
|
||||
console.log('Current page:', currentPage);
|
||||
|
||||
const ws = new WebSocket('ws://' + window.location.host + '/live-reload');
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Live reload connected');
|
||||
ws.send(JSON.stringify({ type: 'register', page: currentPage+".html" }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('Received message:', event.data);
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'reload' && data.content) {
|
||||
console.log('Received new content, updating DOM...');
|
||||
const parser = new DOMParser();
|
||||
const newDoc = parser.parseFromString(data.content, 'text/html');
|
||||
|
||||
if (document.title !== newDoc.title) {
|
||||
document.title = newDoc.title;
|
||||
}
|
||||
|
||||
const stylePromises = [];
|
||||
const newStyles = Array.from(newDoc.getElementsByTagName('link'))
|
||||
.filter(link => link.rel === 'stylesheet');
|
||||
|
||||
newStyles.forEach(style => {
|
||||
const newHref = style.href + '?t=' + Date.now();
|
||||
stylePromises.push(new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = newHref;
|
||||
link.onload = () => resolve(link);
|
||||
link.onerror = reject;
|
||||
link.media = 'print';
|
||||
document.head.appendChild(link);
|
||||
}));
|
||||
});
|
||||
|
||||
Promise.all(stylePromises)
|
||||
.then(newLinks => {
|
||||
Array.from(document.getElementsByTagName('link'))
|
||||
.forEach(link => {
|
||||
if (link.rel === 'stylesheet' && !newLinks.includes(link)) {
|
||||
link.remove();
|
||||
}
|
||||
});
|
||||
|
||||
newLinks.forEach(link => {
|
||||
link.media = 'all';
|
||||
});
|
||||
|
||||
document.body.innerHTML = newDoc.body.innerHTML;
|
||||
|
||||
Array.from(newDoc.getElementsByTagName('script'))
|
||||
.forEach(script => {
|
||||
if (!script.src && script.parentElement.tagName === 'BODY') {
|
||||
const newScript = document.createElement('script');
|
||||
newScript.textContent = script.textContent;
|
||||
document.body.appendChild(newScript);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('DOM update complete');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading new stylesheets:', error);
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('Live reload connection closed, attempting to reconnect...');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
`;
|
||||
html = html.replace("</head>", script + "</head>");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
return res.send(html);
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
this.app.use(express.static(this.options.outDir));
|
||||
|
||||
this.app.get("*", (req, res, next) => {
|
||||
if (path.extname(req.path)) return next();
|
||||
|
||||
const htmlPath = path.join(this.options.outDir, req.path + ".html");
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
res.sendFile(htmlPath);
|
||||
} else if (req.path === "/") {
|
||||
const pages = fs
|
||||
.readdirSync(this.options.outDir)
|
||||
.filter((f) => f.endsWith(".html"))
|
||||
.map((f) => f.replace(".html", ""));
|
||||
res.send(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Blueprint Pages</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, system-ui, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
background: #0d1117;
|
||||
color: #e6edf3;
|
||||
}
|
||||
h1 { margin-bottom: 2rem; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { margin: 1rem 0; }
|
||||
a {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Blueprint Pages</h1>
|
||||
<ul>
|
||||
${pages
|
||||
.map((page) => `<li><a href="/${page}">${page}</a></li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.liveReload) {
|
||||
this.app.ws("/live-reload", (ws, req) => {
|
||||
ws.on("message", (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg);
|
||||
if (data.type === "register" && data.page) {
|
||||
this.clients.set(ws, data.page);
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
this.clients.delete(ws);
|
||||
});
|
||||
|
||||
ws.on("error", (error) => {
|
||||
this.log("ERROR", "WebSocket error:", "red");
|
||||
this.clients.delete(ws);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupWatcher(watcher) {
|
||||
watcher.on("change", async (filepath) => {
|
||||
if (filepath.endsWith(".bp")) {
|
||||
this.log("INFO", `File ${filepath} has been changed`, "blue");
|
||||
try {
|
||||
const builder = new BlueprintBuilder({
|
||||
minified: this.options.minified,
|
||||
debug: this.options.debug,
|
||||
});
|
||||
const relativePath = path.relative(this.options.srcDir, filepath);
|
||||
const outputPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
const result = builder.build(filepath, path.dirname(outputPath));
|
||||
|
||||
if (result.success) {
|
||||
this.log("SUCCESS", "Rebuilt successfully", "green");
|
||||
|
||||
this.filesWithErrors.delete(filepath);
|
||||
|
||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
||||
const htmlPath = path.join(this.options.outDir, htmlFile);
|
||||
|
||||
try {
|
||||
const newContent = fs.readFileSync(htmlPath, "utf8");
|
||||
|
||||
for (const [client, page] of this.clients.entries()) {
|
||||
if (
|
||||
page === htmlFile.replace(/\\/g, "/") &&
|
||||
client.readyState === 1
|
||||
) {
|
||||
try {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "reload",
|
||||
content: newContent,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error sending content:", "red");
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error reading new content:", "red");
|
||||
}
|
||||
} else {
|
||||
this.filesWithErrors.add(filepath);
|
||||
this.log("ERROR", `Build failed: ${result.errors.map(e => e.message).join(", ")}`, "red");
|
||||
this.log("INFO", "Waiting for next file change...", "orange");
|
||||
|
||||
for (const [client, page] of this.clients.entries()) {
|
||||
const htmlFile = relativePath.replace(/\.bp$/, ".html");
|
||||
if (
|
||||
page === htmlFile.replace(/\\/g, "/") &&
|
||||
client.readyState === 1
|
||||
) {
|
||||
try {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "buildError",
|
||||
errors: result.errors,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Error sending error notification:", "red");
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Unexpected error during build:", "red");
|
||||
this.filesWithErrors.add(filepath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on("add", async (filepath) => {
|
||||
if (filepath.endsWith(".bp")) {
|
||||
this.log("INFO", `New file detected: ${filepath}`, "lightGray");
|
||||
try {
|
||||
const builder = new BlueprintBuilder({
|
||||
minified: this.options.minified,
|
||||
debug: this.options.debug,
|
||||
});
|
||||
const relativePath = path.relative(this.options.srcDir, filepath);
|
||||
const outputPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
this.ensureDirectoryExistence(outputPath);
|
||||
const result = builder.build(filepath, path.dirname(outputPath));
|
||||
if (result.success) {
|
||||
this.log("SUCCESS", "Built new file successfully", "green");
|
||||
this.filesWithErrors.delete(filepath);
|
||||
} else {
|
||||
this.filesWithErrors.add(filepath);
|
||||
this.log("ERROR", "Build failed for new file", "red");
|
||||
}
|
||||
} catch (error) {
|
||||
this.log("ERROR", "Unexpected error building new file:", "red");
|
||||
this.filesWithErrors.add(filepath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on("unlink", async (filepath) => {
|
||||
if (filepath.endsWith(".bp")) {
|
||||
this.log("INFO", `File ${filepath} removed`, "orange");
|
||||
const relativePath = path.relative(this.options.srcDir, filepath);
|
||||
const htmlPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".html")
|
||||
);
|
||||
const cssPath = path.join(
|
||||
this.options.outDir,
|
||||
relativePath.replace(/\.bp$/, ".css")
|
||||
);
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
fs.unlinkSync(htmlPath);
|
||||
}
|
||||
if (fs.existsSync(cssPath)) {
|
||||
fs.unlinkSync(cssPath);
|
||||
}
|
||||
const dirPath = path.dirname(htmlPath);
|
||||
if (fs.existsSync(dirPath) && fs.readdirSync(dirPath).length === 0) {
|
||||
fs.rmdirSync(dirPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.buildAll();
|
||||
this.app.listen(this.options.port, () => {
|
||||
this.log(
|
||||
"INFO",
|
||||
`Blueprint dev server running at http://localhost:${this.options.port}`,
|
||||
"green"
|
||||
);
|
||||
this.log(
|
||||
"INFO",
|
||||
`Mode: ${this.options.minified ? "Minified" : "Human Readable"}`,
|
||||
"lightGray"
|
||||
);
|
||||
if (this.options.liveReload) {
|
||||
this.log(
|
||||
"INFO",
|
||||
"Live reload enabled - watching for changes...",
|
||||
"lightGray"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintServer;
|
28
package.json
Normal file
28
package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "blueprint",
|
||||
"version": "1.0.0",
|
||||
"description": "A modern UI component compiler with live reload support",
|
||||
"main": "lib/BlueprintBuilder.js",
|
||||
"scripts": {
|
||||
"build": "node lib/build.js --debug && echo \"Built successfully! You can now deploy the dist folder.\" && exit 0",
|
||||
"dev": "node lib/dev-server.js --live"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"chokidar": "^3.5.3",
|
||||
"express": "^4.18.2",
|
||||
"express-ws": "^5.0.2",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"keywords": [
|
||||
"ui",
|
||||
"compiler",
|
||||
"live-reload",
|
||||
"development"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
15
src/index.bp
Normal file
15
src/index.bp
Normal file
|
@ -0,0 +1,15 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Modern Web UI Language" }
|
||||
description { "It works!" }
|
||||
keywords { "blueprint, language, web, ui, modern" }
|
||||
author { "Epilogue Team" }
|
||||
}
|
||||
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(centered,wide) {
|
||||
title(huge) { "It works!" }
|
||||
text(subtle, large) { "Start using Blueprint at src/" }
|
||||
text(small) { "Find examples at examples/" }
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue