This commit is contained in:
obvTiger 2025-01-21 14:00:09 +01:00
commit 47f67eea8c
43 changed files with 5819 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
dist/
node_modules/
package-lock.json
bun.lockb

34
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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>

View 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" }
}
}
}

View 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" }
}
}
}

View 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 { "📱" } }
}
}

View 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" } }
}
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

3
extension/client/out/extension.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
import { ExtensionContext } from 'vscode';
export declare function activate(context: ExtensionContext): void;
export declare function deactivate(): Thenable<void> | undefined;

View 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

View 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"}

View 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();
}

View 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"]
}

View 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();
}

View file

@ -0,0 +1 @@
{"root":["../client/src/extension.ts"],"version":"5.7.2"}

View file

@ -0,0 +1 @@
{"root":["../client/src/extension.ts","../server/src/server.ts"],"version":"5.7.3"}

49
extension/package.json Normal file
View 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"
}
}

File diff suppressed because one or more lines are too long

1
extension/server/out/server.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

View 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

File diff suppressed because one or more lines are too long

View 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();

View 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"]
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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/" }
}
}