feat: reactivityl; vdom; rhei; styles; fs; image element
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
5397
Cargo.lock
generated
Normal file
5397
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "glint-runtime"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
glt = { path = "../glt" }
|
||||
regex = "1.10"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
iced = { version = "0.14.0", features = ["tokio", "image", "svg"] }
|
||||
colored = "2"
|
||||
indicatif = "0.17"
|
||||
rhai = "1.17.1"
|
||||
chrono = "0.4.44"
|
||||
207
desktop.gltm
Normal file
207
desktop.gltm
Normal file
@@ -0,0 +1,207 @@
|
||||
// =============================================================================
|
||||
// GLINT UI: Ultimate Reactive Showcase Architecture
|
||||
// =============================================================================
|
||||
|
||||
@version 1
|
||||
@style "main.glts"
|
||||
|
||||
// ── 1. Global Configuration & Constants ──────────────────────────────────────
|
||||
|
||||
@global $APP_TITLE = "Glint Design System & Rhei Demo"
|
||||
@global $IS_PRODUCTION = false
|
||||
|
||||
// Глобальное состояние приложения
|
||||
@global $access_level = 3.0
|
||||
@global $username = "Guest"
|
||||
@global $volume_level = 0.0
|
||||
@global $is_enabled = false
|
||||
@global $search_query = "Type to search..."
|
||||
@global $team_members = ["Alexander", "Beatrice", "Cyrus", "Diana"]
|
||||
|
||||
@singleton SystemSettings {
|
||||
theme = "ocean-blue",
|
||||
refresh_rate = 60,
|
||||
debug_mode = true,
|
||||
storage_path = fs:/var/lib/glint/assets
|
||||
}
|
||||
|
||||
// ── 2. Logic Layer (Document-level Rhai Script) ──────────────────────────────
|
||||
|
||||
!rhei: {
|
||||
// Вспомогательные UI-функции
|
||||
fn level_label(lvl) {
|
||||
if lvl >= 8.0 { "Admin" }
|
||||
else if lvl >= 5.0 { "Moderator" }
|
||||
else { "Guest" }
|
||||
}
|
||||
|
||||
fn volume_icon(vol) {
|
||||
if vol == 0.0 { "🔇" }
|
||||
else if vol < 40.0 { "🔈" }
|
||||
else if vol < 75.0 { "🔉" }
|
||||
else { "🔊" }
|
||||
}
|
||||
|
||||
// Демонстрация сложных вычислений при загрузке документа
|
||||
const TARGET = 20;
|
||||
fn fib(n) {
|
||||
if n < 2 { return n; }
|
||||
let a = 0; let b = 1;
|
||||
for i in 2..=n {
|
||||
let c = a + b;
|
||||
a = b; b = c;
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
print(`Initializing Glint Engine...`);
|
||||
let result = fib(TARGET);
|
||||
print(`Fibonacci(${TARGET}) computed on load: ${result}`);
|
||||
}
|
||||
|
||||
// ── 3. UI Components ─────────────────────────────────────────────────────────
|
||||
|
||||
@component ProfileCard(username: String, access_level: Float) {
|
||||
Panel {
|
||||
Header !rhei: { "User: " + username }
|
||||
Label(text=$username)
|
||||
|
||||
// Сложное условие в Rhai: вычисляется при каждом обновлении VDOM
|
||||
@if !rhei: { (access_level >= 5.0) && (username.len() > 0) } {
|
||||
Text "✅ Administrator Privileges Active"
|
||||
Text !rhei: { "Role: " + level_label(access_level) }
|
||||
} @else {
|
||||
Text "🔒 Restricted Access Mode"
|
||||
Text "Role: Guest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Main Application Tree ─────────────────────────────────────────────────
|
||||
|
||||
Window(
|
||||
title=$APP_TITLE,
|
||||
width=1280,
|
||||
height=800,
|
||||
resizable=true
|
||||
) {
|
||||
// Локальное состояние окна
|
||||
@let $show_metrics = true
|
||||
|
||||
Panel(id="main_viewport", padding=24) {
|
||||
|
||||
Header "Dashboard Overview"
|
||||
Text "Welcome to the ultimate Glint component test suite."
|
||||
Divider()
|
||||
|
||||
// ── Секция A: Control Panel (Переменные и биндинги) ──────────────────
|
||||
Header "A. Control Panel & State Bindings"
|
||||
|
||||
Image(src=fs:/home/faynot/elyz/software/glt/logo.png) {}
|
||||
|
||||
Panel {
|
||||
Input(placeholder=$search_query, value=$search_query)
|
||||
Toggle(label="Enable Live Metrics", value=$is_enabled)
|
||||
Text !rhei: { "Live search query: " + search_query }
|
||||
}
|
||||
|
||||
@if $is_enabled {
|
||||
Text "✅ Features are ON. You can adjust the system."
|
||||
} @else {
|
||||
Text "⛔ Features are OFF."
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// ── Секция B: Arithmetic Conditions & Reactive Text ──────────────────
|
||||
Header "B. Rhei Arithmetic Conditions"
|
||||
|
||||
Slider(value=$volume_level)
|
||||
ProgressBar(value=$volume_level)
|
||||
|
||||
// Трехуровневое ветвление с использованием Rhai-условий
|
||||
@if !rhei: { volume_level == 0.0 } {
|
||||
Text "🔇 Muted"
|
||||
} @else {
|
||||
@if !rhei: { volume_level >= 75.0 } {
|
||||
Text "🔊 High volume — protect your hearing!"
|
||||
} @else {
|
||||
// Инлайн-текст, вызывающий функцию из глобального скрипта
|
||||
Text !rhei: { volume_icon(volume_level) + " Volume OK: " + volume_level + " / 100" }
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// ── Секция C: Access Control & Component Instantiation ───────────────
|
||||
Header "C. Multi-variable Logic & Components"
|
||||
|
||||
Slider(value=$access_level)
|
||||
|
||||
!rhei: { "Current slider level: " + access_level + " → " + level_label(access_level) }
|
||||
|
||||
Panel {
|
||||
Button(label="Grant Admin (Level 9)") {
|
||||
@on click {
|
||||
!rhei: { access_level = 9.0; }
|
||||
}
|
||||
}
|
||||
Button(label="Reset (Level 1)") {
|
||||
@on click {
|
||||
!rhei: { access_level = 1.0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if !rhei: {
|
||||
let threshold = 5.0;
|
||||
access_level >= threshold
|
||||
} {
|
||||
Text "🛡️ Access granted — Privileged Zone"
|
||||
ProfileCard(username=$username, access_level=$access_level)
|
||||
} @else {
|
||||
Text "🚫 Access denied — Raise your level to 5+"
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// ── Секция D: Dynamic List Rendering (@each) ─────────────────────────
|
||||
Header "D. Team Management (Reactive Grid)"
|
||||
|
||||
// Передаем direction и columns как свойства компонента
|
||||
Panel(columns=2, direction="grid") {
|
||||
@each $name in $team_members {
|
||||
Panel {
|
||||
ProfileCard(username=$name, access_level=$access_level)
|
||||
|
||||
Button(label="Promote System-wide") {
|
||||
@on click {
|
||||
!rhei: {
|
||||
// VDOM автоматически заменит "$name" на "Alexander", "Beatrice" и т.д.
|
||||
let target_name = "$name";
|
||||
print("Promoting triggered by " + target_name);
|
||||
|
||||
if access_level < 10.0 {
|
||||
access_level += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// ── Секция E: Footer Metadata ────────────────────────────────────────
|
||||
Panel {
|
||||
Text "System Version: 1.0.5-stable"
|
||||
|
||||
// Многострочный скрипт прямо внутри текстового узла
|
||||
!rhei: {
|
||||
let pct = (access_level * 10.0).to_string();
|
||||
"Access Power: " + pct + "% | Integrity: OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
main.glts
Normal file
106
main.glts
Normal file
@@ -0,0 +1,106 @@
|
||||
// =============================================================================
|
||||
// Glint Design System — Master Stylesheet (main.glts)
|
||||
// =============================================================================
|
||||
|
||||
// ── 1. Цветовая палитра и константы (Variables) ──────────────────────────────
|
||||
$bg-app = #11111b // Глубокий темный фон для всего окна
|
||||
$bg-panel = #1e1e2e // Базовый цвет для контейнеров и панелей
|
||||
$bg-surface = #313244 // Цвет для карточек и выделенных блоков
|
||||
$bg-field = #181825 // Внутренний фон для полей ввода (Input)
|
||||
|
||||
$text-main = #cdd6f4 // Мягкий белый основной текст
|
||||
$text-muted = #9399b2 // Серый приглушенный текст для описаний
|
||||
$accent = #b4befe // Лавандовый акцентный цвет для кнопок и заголовков
|
||||
$border-glow = #45475a // Цвет аккуратных рамок и разделителей
|
||||
|
||||
// Сетка отступов (Размеры автоматически очищаются от "px" парсером Glint)
|
||||
$pad-dense = 6px
|
||||
$pad-normal = 12px
|
||||
$pad-roomy = 20px
|
||||
$pad-window = 28px
|
||||
|
||||
// ── 2. Шаблоны стилей (Mixins) ───────────────────────────────────────────────
|
||||
|
||||
@mixin text-body {
|
||||
color: $text-main
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
@mixin standard-card {
|
||||
background: $bg-panel
|
||||
border-color: $border-glow
|
||||
border-width: 1px
|
||||
border-radius: 12px
|
||||
}
|
||||
|
||||
// ── 3. Правила для UI-компонентов (Element Rules) ────────────────────────────
|
||||
|
||||
// Главное окно приложения
|
||||
Window {
|
||||
background: $bg-app
|
||||
padding: $pad-window
|
||||
spacing: $pad-roomy
|
||||
}
|
||||
|
||||
// Универсальный контейнер ( viewport, карточки, обертки списков )
|
||||
Panel {
|
||||
@use standard-card
|
||||
padding: $pad-normal
|
||||
gap: 14px
|
||||
direction: vertical
|
||||
align-items: center
|
||||
content-align: center
|
||||
}
|
||||
|
||||
// Крупные заголовки секций
|
||||
Header {
|
||||
color: $accent
|
||||
font-size: 22px
|
||||
padding: 4px
|
||||
}
|
||||
|
||||
// Обычный текст
|
||||
Text {
|
||||
@use text-body
|
||||
}
|
||||
|
||||
// Подписи, второстепенные метаданные
|
||||
Label {
|
||||
color: $text-muted
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
// Интерактивные элементы управления
|
||||
Button {
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
}
|
||||
|
||||
Image {
|
||||
border-radius: 8px
|
||||
border-width: 1px
|
||||
}
|
||||
|
||||
// Текстовые поля ввода
|
||||
Input {
|
||||
background-color: $bg-field
|
||||
color: $text-main
|
||||
font-size: 14px
|
||||
padding: 12px
|
||||
border-radius: 16px
|
||||
border-width: 1px
|
||||
border-color: $border-glow
|
||||
width: 320px
|
||||
}
|
||||
|
||||
// Переключатели
|
||||
Toggle {
|
||||
color: $text-main
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
// Разделительные линии (Divider / Separator)
|
||||
Divider {
|
||||
background: $border-glow
|
||||
border-width: 1px
|
||||
}
|
||||
95
src/app.rs
Normal file
95
src/app.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use iced::widget::{column, container, row, scrollable, text, Space};
|
||||
use iced::{Alignment, Length, Theme};
|
||||
|
||||
use crate::interpreter::{Document, Element, Interpreter, RheiContext};
|
||||
use crate::renderer::render_element;
|
||||
use crate::Message;
|
||||
|
||||
pub struct GlintApp {
|
||||
pub doc: Document,
|
||||
pub vdom_roots: Vec<Element>,
|
||||
pub rhei: RheiContext,
|
||||
}
|
||||
|
||||
impl GlintApp {
|
||||
pub fn title(&self) -> String {
|
||||
"Glint Runtime - Native Renderer".to_string()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> iced::Task<Message> {
|
||||
match message {
|
||||
Message::EventTriggered(script) => {
|
||||
if !script.is_empty() {
|
||||
self.rhei.execute_action(&script, &mut self.doc.variables);
|
||||
}
|
||||
}
|
||||
Message::InputChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var, val);
|
||||
}
|
||||
Message::ToggleChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var, val.to_string());
|
||||
}
|
||||
Message::SliderChanged(Some(var), val) => {
|
||||
self.doc.variables.insert(var.clone(), format!("{:.1}", val));
|
||||
if var == "volume_level" {
|
||||
self.doc.variables.insert("age".to_string(), val.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.vdom_roots = Interpreter::evaluate_vdom(
|
||||
&self.doc.roots,
|
||||
&self.doc.variables,
|
||||
&self.doc.components,
|
||||
&self.rhei,
|
||||
&self.doc.stylesheet,
|
||||
);
|
||||
|
||||
iced::Task::none()
|
||||
}
|
||||
|
||||
pub fn view(&self) -> iced::Element<'_, Message, Theme, iced::Renderer> {
|
||||
let mut content = column![].spacing(15).padding(20);
|
||||
|
||||
for root in &self.vdom_roots {
|
||||
if let Some(el) = render_element(root) {
|
||||
content = content.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
let status_bar = container(self.build_status_bar(&self.doc.variables))
|
||||
.width(Length::Fill)
|
||||
.padding(10);
|
||||
|
||||
let layout = column![
|
||||
scrollable(content).width(Length::Fill).height(Length::Fill),
|
||||
iced::widget::rule::horizontal(1),
|
||||
status_bar,
|
||||
];
|
||||
|
||||
container(layout)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_status_bar<'a>(
|
||||
&self,
|
||||
scope: &std::collections::HashMap<String, String>,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let slider_val = scope.get("volume_level")
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
row![
|
||||
text("🟢 Runtime Active (VDOM + Rhai)").size(14),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!("Шаблонных узлов: {}", self.doc.roots.len())).size(14),
|
||||
Space::new().width(Length::Fixed(15.0)),
|
||||
text(format!("Слайдер ($volume_level): {:.1}", slider_val)).size(14),
|
||||
]
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
250
src/cli.rs
Normal file
250
src/cli.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use colored::Colorize;
|
||||
use glt::{Compiler, ModuleSoA, NodeId, Parser, StyleParser};
|
||||
use glt::ast::Directive;
|
||||
use iced::Theme;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
use crate::app::GlintApp;
|
||||
use crate::interpreter::{Interpreter, RheiContext};
|
||||
|
||||
pub fn compile_files(gltm_files: &[String], glts_files: &[String], output_path: &str) {
|
||||
let mut glts_source = String::new();
|
||||
for path in glts_files {
|
||||
glts_source.push_str(&read_source(path, "style"));
|
||||
glts_source.push('\n');
|
||||
}
|
||||
|
||||
let mut gltm_source = String::new();
|
||||
for path in gltm_files {
|
||||
gltm_source.push_str(&read_source(path, "markup"));
|
||||
gltm_source.push('\n');
|
||||
}
|
||||
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.set_style(
|
||||
ProgressStyle::with_template("{spinner} {msg}")
|
||||
.unwrap()
|
||||
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
|
||||
);
|
||||
spinner.set_message("Parsing...");
|
||||
spinner.enable_steady_tick(Duration::from_millis(80));
|
||||
|
||||
let parse_start = Instant::now();
|
||||
|
||||
let mut module = ModuleSoA::new();
|
||||
|
||||
if !glts_source.is_empty() {
|
||||
let mut style_parser = StyleParser::new(&glts_source, &mut module);
|
||||
if let Err(e) = style_parser.parse_all() {
|
||||
spinner.finish_with_message("❌ Style parsing failed".red().to_string());
|
||||
eprintln!("{}", format!("Style Error: {}", e).red());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mut parser = Parser::with_module(&gltm_source, module);
|
||||
|
||||
match parser.parse_all() {
|
||||
Ok(_) => {
|
||||
let parse_dur = parse_start.elapsed();
|
||||
spinner.finish_with_message(format!("✅ Parsing done ({:?})", parse_dur));
|
||||
|
||||
spinner.set_message("Building AST tree...");
|
||||
let tree_start = Instant::now();
|
||||
|
||||
let module = &parser.module;
|
||||
let mut child_indices: HashSet<usize> = HashSet::new();
|
||||
|
||||
for (start, len) in &module.elem_child_spans {
|
||||
let start_us = *start as usize;
|
||||
let len_us = *len as usize;
|
||||
child_indices.extend(start_us..(start_us + len_us));
|
||||
}
|
||||
|
||||
for dir in &module.directives {
|
||||
let spans: Vec<(usize, usize)> = match dir {
|
||||
Directive::Component { child_span, .. } => {
|
||||
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||
}
|
||||
Directive::If { child_span, else_span, .. } => {
|
||||
let mut v = vec![(child_span.0 as usize, child_span.1 as usize)];
|
||||
if let Some(es) = else_span {
|
||||
v.push((es.0 as usize, es.1 as usize));
|
||||
}
|
||||
v
|
||||
}
|
||||
Directive::Each { child_span, .. } => {
|
||||
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||
}
|
||||
Directive::On { child_span, .. } => {
|
||||
vec![(child_span.0 as usize, child_span.1 as usize)]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
for (start, len) in spans {
|
||||
child_indices.extend(start..(start + len));
|
||||
}
|
||||
}
|
||||
|
||||
let root_nodes: Vec<NodeId> = module
|
||||
.hierarchy
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !child_indices.contains(i))
|
||||
.map(|(_, node_id)| *node_id)
|
||||
.collect();
|
||||
|
||||
let tree_dur = tree_start.elapsed();
|
||||
spinner.finish_with_message(format!("✅ Tree built ({:?})", tree_dur));
|
||||
|
||||
spinner.set_message("Generating bytecode...");
|
||||
let compile_start = Instant::now();
|
||||
let bytecode = Compiler::new(&parser.module).compile(&root_nodes);
|
||||
let compile_dur = compile_start.elapsed();
|
||||
spinner.finish_with_message(format!("✅ Bytecode generated ({:?})", compile_dur));
|
||||
|
||||
spinner.set_message("Writing to disk...");
|
||||
let io_start = Instant::now();
|
||||
write_bytecode(output_path, &bytecode);
|
||||
let io_dur = io_start.elapsed();
|
||||
spinner.finish_with_message(format!("✅ Written to {}", output_path));
|
||||
|
||||
let total_dur = parse_dur + tree_dur + compile_dur + io_dur;
|
||||
|
||||
print_stats(BuildStats {
|
||||
parse: parse_dur,
|
||||
tree: tree_dur,
|
||||
compile: compile_dur,
|
||||
io: io_dur,
|
||||
total: total_dur,
|
||||
size: bytecode.len(),
|
||||
output: output_path,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
spinner.finish_with_message("❌ Parsing failed".red().to_string());
|
||||
eprintln!("{}", format!("Markup Error: {}", e).red());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_file(path: &str) -> ! {
|
||||
let bytecode = read_file_or_exit(path);
|
||||
println!("{} {}", "▶️".green(), format!("Rendering: {}", path).bold());
|
||||
|
||||
let start = Instant::now();
|
||||
match Interpreter::run(&bytecode) {
|
||||
Ok(doc) => {
|
||||
println!(
|
||||
"{} {} ({:?})",
|
||||
"✅".green(),
|
||||
"Bytecode decoded".bold(),
|
||||
start.elapsed()
|
||||
);
|
||||
println!(
|
||||
"{} {} Rhai блоков",
|
||||
"📜".green(),
|
||||
doc.rhei_scripts.len()
|
||||
);
|
||||
println!("{}", "🚀 Launching GUI...".green().bold());
|
||||
|
||||
let result = iced::application(
|
||||
move || {
|
||||
let rhei = RheiContext::new(&doc.rhei_scripts);
|
||||
|
||||
let vdom_roots = Interpreter::evaluate_vdom(
|
||||
&doc.roots,
|
||||
&doc.variables,
|
||||
&doc.components,
|
||||
&rhei,
|
||||
&doc.stylesheet,
|
||||
);
|
||||
|
||||
GlintApp {
|
||||
doc: doc.clone(),
|
||||
rhei,
|
||||
vdom_roots,
|
||||
}
|
||||
},
|
||||
GlintApp::update,
|
||||
GlintApp::view,
|
||||
)
|
||||
.title(GlintApp::title)
|
||||
.window(iced::window::Settings {
|
||||
size: iced::Size::new(500.0, 700.0),
|
||||
..Default::default()
|
||||
})
|
||||
.theme(|_: &GlintApp| Theme::Dark)
|
||||
.run();
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("{} {}", "❌".red(), format!("Iced error: {:?}", e).red());
|
||||
std::process::exit(1);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{} {}", "❌".red(), format!("Interpreter error: {}", e).red());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_source(path: &str, kind: &str) -> String {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(s) => {
|
||||
println!("{} {} {}", "📄".green(), kind.cyan(), path.italic());
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file_or_exit(path: &str) -> Vec<u8> {
|
||||
fs::read(path).unwrap_or_else(|e| {
|
||||
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
fn write_bytecode(path: &str, data: &[u8]) {
|
||||
let mut file = File::create(path).unwrap_or_else(|e| {
|
||||
eprintln!("{} {}: {}", "❌".red(), path.red(), e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
file.write_all(data).unwrap_or_else(|e| {
|
||||
eprintln!("{} {}", "❌".red(), format!("Write failed: {}", e).red());
|
||||
std::process::exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
struct BuildStats<'a> {
|
||||
parse: Duration,
|
||||
tree: Duration,
|
||||
compile: Duration,
|
||||
io: Duration,
|
||||
total: Duration,
|
||||
size: usize,
|
||||
output: &'a str,
|
||||
}
|
||||
|
||||
fn print_stats(s: BuildStats) {
|
||||
println!("\n{}", "Compilation Statistics".bold().underline());
|
||||
println!(" {:<15} {}", "Parse:".cyan(), format!("{:?}", s.parse).white());
|
||||
println!(" {:<15} {}", "AST tree:".cyan(), format!("{:?}", s.tree).white());
|
||||
println!(" {:<15} {}", "Bytecode gen:".cyan(), format!("{:?}", s.compile).white());
|
||||
println!(" {:<15} {}", "Disk write:".cyan(), format!("{:?}", s.io).white());
|
||||
println!(" {:<15} {}", "Total time:".green().bold(), format!("{:?}", s.total).green().bold());
|
||||
println!(" {:<15} {}", "Output size:".yellow(), format!("{} bytes", s.size).yellow());
|
||||
println!(" {:<15} {}", "Saved to:".yellow(), s.output.yellow());
|
||||
}
|
||||
496
src/interpreter/mod.rs
Normal file
496
src/interpreter/mod.rs
Normal file
@@ -0,0 +1,496 @@
|
||||
pub mod opcodes;
|
||||
pub mod reader;
|
||||
pub mod rhei;
|
||||
pub mod style;
|
||||
pub mod types;
|
||||
|
||||
pub use rhei::RheiContext;
|
||||
use style::StyleSheet as SS;
|
||||
pub use types::{ComponentDef, Document, Element, InterpError};
|
||||
|
||||
use opcodes::*;
|
||||
use reader::Reader;
|
||||
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX};
|
||||
use style::ComputedStyle;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static RE_VAR: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
pub struct Interpreter;
|
||||
|
||||
impl Interpreter {
|
||||
pub fn run(bytecode: &[u8]) -> Result<Document, InterpError> {
|
||||
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
|
||||
if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); }
|
||||
|
||||
let mut r = Reader::new(bytecode);
|
||||
r.pos = 4;
|
||||
let mut variables = HashMap::new();
|
||||
let mut components = HashMap::new();
|
||||
let mut rhei_scripts: Vec<String> = Vec::new();
|
||||
let mut stylesheet = SS::new();
|
||||
|
||||
let roots = Self::parse_block_elements(
|
||||
&mut r,
|
||||
&mut variables,
|
||||
&mut components,
|
||||
&mut rhei_scripts,
|
||||
&mut stylesheet,
|
||||
true,
|
||||
)?;
|
||||
|
||||
let rhei_ctx = RheiContext::new(&rhei_scripts);
|
||||
rhei_ctx.initialize(&mut variables);
|
||||
|
||||
Ok(Document { roots, components, variables, rhei_scripts, stylesheet })
|
||||
}
|
||||
|
||||
fn parse_block_elements(
|
||||
r: &mut Reader,
|
||||
variables: &mut HashMap<String, String>,
|
||||
components: &mut HashMap<String, ComponentDef>,
|
||||
rhei_scripts: &mut Vec<String>,
|
||||
stylesheet: &mut SS,
|
||||
is_root: bool,
|
||||
) -> Result<Vec<Element>, InterpError> {
|
||||
let mut roots: Vec<Element> = Vec::new();
|
||||
let mut stack: Vec<Element> = Vec::new();
|
||||
|
||||
while r.remaining() > 0 {
|
||||
let op = r.read_byte()?;
|
||||
if !is_root && op == OP_END_BLOCK { break; }
|
||||
|
||||
match op {
|
||||
OP_ELEM_PUSH => stack.push(Element::new(r.read_string()?)),
|
||||
|
||||
OP_ELEM_POP => {
|
||||
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
|
||||
Self::attach(&mut stack, &mut roots, finished);
|
||||
}
|
||||
|
||||
OP_GLOBAL | OP_LET => {
|
||||
let name = r.read_string()?;
|
||||
let vop = r.read_byte()?;
|
||||
if let Some(value) = r.read_value_as_string(vop)? {
|
||||
variables.insert(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
OP_SINGLETON => {
|
||||
let _name = r.read_string()?;
|
||||
let count = r.read_u32()?;
|
||||
for _ in 0..count {
|
||||
r.read_string()?;
|
||||
let vop = r.read_byte()?;
|
||||
r.skip_value(vop)?;
|
||||
}
|
||||
}
|
||||
|
||||
OP_CONTENT => {
|
||||
let vop = r.read_byte()?;
|
||||
if let Some(value) = r.read_value_as_string(vop)? {
|
||||
if let Some(el) = stack.last_mut() {
|
||||
let stored = if vop == OP_PROP_RHEI {
|
||||
format!("{RHEI_PREFIX}{value}")
|
||||
} else {
|
||||
value
|
||||
};
|
||||
el.properties.insert("text".to_string(), stored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OP_PROP_STR => {
|
||||
let (key, val) = (r.read_string()?, r.read_string()?);
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
OP_PROP_VAR => {
|
||||
let key = r.read_string()?;
|
||||
let val = format!("${}", r.read_string()?);
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
OP_PROP_INT => {
|
||||
let key = r.read_string()?;
|
||||
let val = r.read_i64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
OP_PROP_FLOAT => {
|
||||
let key = r.read_string()?;
|
||||
let val = r.read_f64()?.to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
OP_PROP_BOOL => {
|
||||
let key = r.read_string()?;
|
||||
let val = (r.read_byte()? != 0).to_string();
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
OP_PROP_RHEI => {
|
||||
let key = r.read_string()?;
|
||||
let expr = r.read_string()?;
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}"));
|
||||
}
|
||||
}
|
||||
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
|
||||
let key = r.read_string()?;
|
||||
if let Some(val) = r.read_value_as_string(op)? {
|
||||
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
|
||||
}
|
||||
}
|
||||
|
||||
OP_RHEI_BLK => {
|
||||
let script = r.read_string()?;
|
||||
if is_root && stack.is_empty() {
|
||||
rhei_scripts.push(script);
|
||||
} else {
|
||||
let mut text_el = Element::new("#text".to_string());
|
||||
text_el.properties.insert(
|
||||
"text".to_string(),
|
||||
format!("{RHEI_PREFIX}{script}"),
|
||||
);
|
||||
Self::attach(&mut stack, &mut roots, text_el);
|
||||
}
|
||||
}
|
||||
|
||||
OP_COMPONENT => {
|
||||
let name = r.read_string()?;
|
||||
let param_count = r.read_u32()?;
|
||||
let params = (0..param_count)
|
||||
.map(|_| Ok((r.read_string()?, r.read_string()?)))
|
||||
.collect::<Result<Vec<_>, InterpError>>()?;
|
||||
let children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
)?;
|
||||
components.insert(name.clone(), ComponentDef { name, params, children });
|
||||
}
|
||||
|
||||
OP_IF => {
|
||||
let vop = r.read_byte()?;
|
||||
let raw = r.read_value_as_string(vop)?.unwrap_or_default();
|
||||
let cond_val = if vop == OP_PROP_RHEI {
|
||||
format!("{RHEI_PREFIX}{raw}")
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
|
||||
let true_children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
)?;
|
||||
let has_else = r.read_byte()? == 1;
|
||||
let false_children = if has_else {
|
||||
Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut if_el = Element::new("@if".to_string());
|
||||
if_el.properties.insert("condition".to_string(), cond_val);
|
||||
if_el.children = true_children;
|
||||
|
||||
if !false_children.is_empty() {
|
||||
let mut else_el = Element::new("@else".to_string());
|
||||
else_el.children = false_children;
|
||||
if_el.children.push(else_el);
|
||||
}
|
||||
|
||||
Self::attach(&mut stack, &mut roots, if_el);
|
||||
}
|
||||
|
||||
OP_EACH => {
|
||||
let var_name = r.read_string()?;
|
||||
let vop = r.read_byte()?;
|
||||
let source_val = match vop {
|
||||
OP_PROP_VAR => format!("${}", r.read_string()?),
|
||||
OP_PROP_ARRAY => r.read_array_as_strings()?.join(","),
|
||||
OP_PROP_RHEI => format!("{RHEI_PREFIX}{}", r.read_string()?),
|
||||
_ => { r.skip_value(vop)?; String::new() }
|
||||
};
|
||||
|
||||
let block_children = Self::parse_block_elements(
|
||||
r, variables, components, rhei_scripts, stylesheet, false,
|
||||
)?;
|
||||
|
||||
let mut each_el = Element::new("@each".to_string());
|
||||
each_el.properties.insert("var_name".to_string(), var_name);
|
||||
each_el.properties.insert("source".to_string(), source_val);
|
||||
each_el.children = block_children;
|
||||
Self::attach(&mut stack, &mut roots, each_el);
|
||||
}
|
||||
|
||||
OP_ON => {
|
||||
let event_name = r.read_string()?;
|
||||
let arg_count = r.read_u32()?;
|
||||
let mut _args: Vec<(String, String)> = Vec::with_capacity(arg_count as usize);
|
||||
for _ in 0..arg_count {
|
||||
let k = r.read_string()?;
|
||||
let vop = r.read_byte()?;
|
||||
if let Some(v) = r.read_value_as_string(vop)? {
|
||||
_args.push((k, v));
|
||||
}
|
||||
}
|
||||
|
||||
let mut handler_script = String::new();
|
||||
loop {
|
||||
let inner_op = r.read_byte()?;
|
||||
if inner_op == OP_END_BLOCK { break; }
|
||||
if inner_op == OP_RHEI_BLK {
|
||||
handler_script = r.read_string()?;
|
||||
} else {
|
||||
r.skip_opcode(inner_op)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(el) = stack.last_mut() {
|
||||
el.properties.insert(
|
||||
format!("__on:{event_name}"),
|
||||
handler_script,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OP_STYLE_RULE => {
|
||||
let selector = r.read_string()?;
|
||||
let prop_count = r.read_u32()?;
|
||||
let mut props = HashMap::with_capacity(prop_count as usize);
|
||||
|
||||
for _ in 0..prop_count {
|
||||
let key = r.read_string()?;
|
||||
let type_op = r.read_byte()?;
|
||||
if let Some(val) = r.read_value_as_string(type_op)? {
|
||||
props.insert(key, val);
|
||||
}
|
||||
}
|
||||
stylesheet.add_rule(selector, props);
|
||||
}
|
||||
|
||||
OP_STYLE_ANIM => {
|
||||
r.skip_opcode(OP_STYLE_ANIM)?;
|
||||
}
|
||||
|
||||
other => r.skip_opcode(other)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
pub fn evaluate_vdom(
|
||||
templates: &[Element],
|
||||
variables: &HashMap<String, String>,
|
||||
components: &HashMap<String, ComponentDef>,
|
||||
rhei: &RheiContext,
|
||||
stylesheet: &SS,
|
||||
) -> Vec<Element> {
|
||||
let mut output = Vec::with_capacity(templates.len());
|
||||
|
||||
for el in templates {
|
||||
match el.type_name.as_str() {
|
||||
"@if" => {
|
||||
let cond = el.properties.get("condition").cloned().unwrap_or_default();
|
||||
let is_true = Self::evaluate_condition(&cond, variables, rhei);
|
||||
|
||||
let mut active_branch = Vec::new();
|
||||
for child in &el.children {
|
||||
if child.type_name == "@else" {
|
||||
if !is_true { active_branch.extend(child.children.clone()); }
|
||||
} else if is_true {
|
||||
active_branch.push(child.clone());
|
||||
}
|
||||
}
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&active_branch, variables, components, rhei, stylesheet,
|
||||
));
|
||||
}
|
||||
|
||||
"@each" => {
|
||||
let var_name = el.properties.get("var_name").cloned().unwrap_or_default();
|
||||
let source_expr = el.properties.get("source").cloned().unwrap_or_default();
|
||||
|
||||
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables)
|
||||
.map(|s| Self::normalize_rhai_array(&s))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Self::resolve_string(&source_expr, variables)
|
||||
};
|
||||
|
||||
let items: Vec<String> = if resolved_source.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
resolved_source.split(',').map(str::trim).map(str::to_string).collect()
|
||||
};
|
||||
|
||||
for item in items {
|
||||
let mut local_vars = variables.clone();
|
||||
local_vars.insert(var_name.clone(), item);
|
||||
output.extend(Self::evaluate_vdom(
|
||||
&el.children, &local_vars, components, rhei, stylesheet,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if let Some(comp) = components.get(&el.type_name) {
|
||||
// Expand custom component
|
||||
let mut comp_scope = variables.clone();
|
||||
for (param, _) in &comp.params {
|
||||
if let Some(arg) = el.properties.get(param) {
|
||||
comp_scope.insert(
|
||||
param.clone(),
|
||||
Self::resolve_prop(arg, variables, rhei),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut vcomp = Element::new(el.type_name.clone());
|
||||
for (k, v) in &el.properties {
|
||||
if k.starts_with("__on:") {
|
||||
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||
} else {
|
||||
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei));
|
||||
}
|
||||
}
|
||||
|
||||
vcomp.computed_style = ComputedStyle::compute(
|
||||
&vcomp.properties,
|
||||
stylesheet.resolve(&vcomp.type_name),
|
||||
);
|
||||
vcomp.children = Self::evaluate_vdom(
|
||||
&comp.children, &comp_scope, components, rhei, stylesheet,
|
||||
);
|
||||
output.push(vcomp);
|
||||
} else {
|
||||
let mut vnode = Element::new(el.type_name.clone());
|
||||
|
||||
for (k, v) in &el.properties {
|
||||
if k.starts_with("__on:") {
|
||||
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables));
|
||||
continue;
|
||||
}
|
||||
if v.starts_with('$')
|
||||
&& !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||
{
|
||||
vnode.properties.insert(
|
||||
format!("__bind:{k}"),
|
||||
v[1..].to_string(),
|
||||
);
|
||||
}
|
||||
vnode.properties.insert(
|
||||
k.clone(),
|
||||
Self::resolve_prop(v, variables, rhei),
|
||||
);
|
||||
}
|
||||
|
||||
vnode.computed_style = ComputedStyle::compute(
|
||||
&vnode.properties,
|
||||
stylesheet.resolve(&el.type_name),
|
||||
);
|
||||
|
||||
vnode.children = Self::evaluate_vdom(
|
||||
&el.children, variables, components, rhei, stylesheet,
|
||||
);
|
||||
output.push(vnode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
|
||||
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
||||
rhei.eval_expr(expr, variables).unwrap_or_default()
|
||||
} else {
|
||||
Self::resolve_string(v, variables)
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_condition(
|
||||
cond: &str,
|
||||
variables: &HashMap<String, String>,
|
||||
rhei: &RheiContext,
|
||||
) -> bool {
|
||||
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
|
||||
return rhei.eval_condition(expr, variables);
|
||||
}
|
||||
|
||||
let mut clean = cond.to_string();
|
||||
if let Some(start) = clean.find('{') {
|
||||
if let Some(end) = clean.rfind('}') {
|
||||
clean = clean[start + 1..end].trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let resolved = Self::resolve_string(&clean, variables).trim().to_string();
|
||||
|
||||
if Self::is_truthy(&resolved) { return true; }
|
||||
if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
|
||||
|
||||
let operators: [(&str, fn(f64, f64) -> bool); 6] = [
|
||||
(">=", |a, b| a >= b),
|
||||
("<=", |a, b| a <= b),
|
||||
(">", |a, b| a > b),
|
||||
("<", |a, b| a < b),
|
||||
("==", |a, b| a == b),
|
||||
("!=", |a, b| a != b),
|
||||
];
|
||||
|
||||
for (op, compare) in operators {
|
||||
if let Some((left, right)) = resolved.split_once(op) {
|
||||
let l = left.trim();
|
||||
let r = right.trim();
|
||||
if let (Ok(ln), Ok(rn)) = (l.parse::<f64>(), r.parse::<f64>()) {
|
||||
return compare(ln, rn);
|
||||
}
|
||||
if op == "==" { return l == r; }
|
||||
if op == "!=" { return l != r; }
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_truthy(s: &str) -> bool {
|
||||
match s.trim() {
|
||||
"" | "false" | "0" | "null" => false,
|
||||
"true" | "1" => true,
|
||||
other => other.parse::<f64>().map(|n| n != 0.0).unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_rhai_array(s: &str) -> String {
|
||||
let trimmed = s.trim();
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
||||
trimmed[1..trimmed.len() - 1]
|
||||
.split(',')
|
||||
.map(|item| item.trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_string(val: &str, scope: &HashMap<String, String>) -> String {
|
||||
let re = RE_VAR.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap());
|
||||
re.replace_all(val, |caps: ®ex::Captures| {
|
||||
scope.get(&caps[1])
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(&caps[0])
|
||||
.to_string()
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) {
|
||||
match stack.last_mut() {
|
||||
Some(parent) => parent.children.push(el),
|
||||
None => roots.push(el),
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/interpreter/opcodes.rs
Normal file
41
src/interpreter/opcodes.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
pub const MAGIC: &[u8; 4] = b"GLBC";
|
||||
pub const MAGIC_HEADER: [u8; 4] = *b"GLBC";
|
||||
|
||||
// Control directives
|
||||
pub const OP_VERSION: u8 = 0x01;
|
||||
pub const OP_STYLE: u8 = 0x02;
|
||||
pub const OP_GLOBAL: u8 = 0x03;
|
||||
pub const OP_SINGLETON: u8 = 0x04;
|
||||
pub const OP_COMPONENT: u8 = 0x05;
|
||||
pub const OP_LET: u8 = 0x06;
|
||||
pub const OP_IF: u8 = 0x07;
|
||||
pub const OP_EACH: u8 = 0x08;
|
||||
pub const OP_ON: u8 = 0x09;
|
||||
pub const OP_RHEI_BLK: u8 = 0x0A;
|
||||
|
||||
pub const OP_STYLE_RULE: u8 = 0x0B;
|
||||
pub const OP_STYLE_ANIM: u8 = 0x0C;
|
||||
|
||||
// Element tree
|
||||
pub const OP_ELEM_PUSH: u8 = 0x10;
|
||||
pub const OP_ELEM_POP: u8 = 0x11;
|
||||
pub const OP_CONTENT: u8 = 0x12;
|
||||
|
||||
// Value type tags
|
||||
pub const OP_PROP_STR: u8 = 0x20; // UTF-8 string literal
|
||||
pub const OP_PROP_INT: u8 = 0x21; // i64 LE
|
||||
pub const OP_PROP_FLOAT: u8 = 0x22; // f64 LE
|
||||
pub const OP_PROP_BOOL: u8 = 0x23; // u8 (0 or 1)
|
||||
pub const OP_PROP_COLOR: u8 = 0x24; // "#RRGGBB" string
|
||||
pub const OP_PROP_FSPATH: u8 = 0x25; // filesystem path string
|
||||
pub const OP_PROP_VAR: u8 = 0x26; // variable name string (without "$")
|
||||
pub const OP_PROP_RHEI: u8 = 0x27; // Rhai expression string
|
||||
pub const OP_PROP_NULL: u8 = 0x28; // no data follows
|
||||
pub const OP_PROP_ARRAY: u8 = 0x29; // count:u32 + count * (type_op + data)
|
||||
|
||||
pub const OP_PROP_CALL: u8 = 0x2A;
|
||||
pub const OP_PROP_UNIT: u8 = 0x2B;
|
||||
pub const OP_PROP_IDENT: u8 = 0x2C;
|
||||
|
||||
// Block terminator
|
||||
pub const OP_END_BLOCK: u8 = 0xFF;
|
||||
280
src/interpreter/reader.rs
Normal file
280
src/interpreter/reader.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use super::opcodes::*;
|
||||
use super::types::InterpError;
|
||||
|
||||
pub struct Reader<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0 }
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.data.len() - self.pos
|
||||
}
|
||||
|
||||
|
||||
pub fn read_byte(&mut self) -> Result<u8, InterpError> {
|
||||
self.require(1)?;
|
||||
let b = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
pub fn read_u32(&mut self) -> Result<u32, InterpError> {
|
||||
self.require(4)?;
|
||||
let v = u32::from_le_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap());
|
||||
self.pos += 4;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn read_i64(&mut self) -> Result<i64, InterpError> {
|
||||
self.require(8)?;
|
||||
let v = i64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
|
||||
self.pos += 8;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn read_f64(&mut self) -> Result<f64, InterpError> {
|
||||
self.require(8)?;
|
||||
let v = f64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
|
||||
self.pos += 8;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn read_string(&mut self) -> Result<String, InterpError> {
|
||||
let len = self.read_u32()? as usize;
|
||||
self.require(len)?;
|
||||
let s = std::str::from_utf8(&self.data[self.pos..self.pos + len])
|
||||
.map_err(|_| InterpError::InvalidUtf8)?
|
||||
.to_string();
|
||||
self.pos += len;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn read_value_as_string(&mut self, type_op: u8) -> Result<Option<String>, InterpError> {
|
||||
let s = match type_op {
|
||||
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI => {
|
||||
Some(self.read_string()?)
|
||||
}
|
||||
OP_PROP_IDENT => {
|
||||
Some(self.read_string()?)
|
||||
}
|
||||
OP_PROP_VAR => {
|
||||
Some(format!("${}", self.read_string()?))
|
||||
}
|
||||
OP_PROP_INT => {
|
||||
Some(self.read_i64()?.to_string())
|
||||
}
|
||||
OP_PROP_FLOAT => {
|
||||
Some(self.read_f64()?.to_string())
|
||||
}
|
||||
OP_PROP_BOOL => {
|
||||
Some((self.read_byte()? != 0).to_string())
|
||||
}
|
||||
OP_PROP_NULL => None,
|
||||
OP_PROP_ARRAY => {
|
||||
let items = self.read_array_as_strings()?;
|
||||
Some(items.join(","))
|
||||
}
|
||||
OP_PROP_UNIT => {
|
||||
let num = self.read_f64()?;
|
||||
let unit = self.read_string()?;
|
||||
if num.fract() == 0.0 {
|
||||
Some(format!("{}{}", num as i64, unit))
|
||||
} else {
|
||||
Some(format!("{}{}", num, unit))
|
||||
}
|
||||
}
|
||||
OP_PROP_CALL => {
|
||||
let name = self.read_string()?;
|
||||
let arg_count = self.read_u32()? as usize;
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
let op = self.read_byte()?;
|
||||
let val = self.read_value_as_string(op)?
|
||||
.unwrap_or_default();
|
||||
args.push(val);
|
||||
}
|
||||
Some(format!("{}({})", name, args.join(",")))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings.
|
||||
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
|
||||
let count = self.read_u32()? as usize;
|
||||
let mut items = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let elem_op = self.read_byte()?;
|
||||
if let Some(s) = self.read_value_as_string(elem_op)? {
|
||||
items.push(s);
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
|
||||
pub fn skip_value(&mut self, type_op: u8) -> Result<(), InterpError> {
|
||||
match type_op {
|
||||
OP_PROP_STR
|
||||
| OP_PROP_COLOR
|
||||
| OP_PROP_FSPATH
|
||||
| OP_PROP_VAR
|
||||
| OP_PROP_RHEI
|
||||
| OP_PROP_IDENT => {
|
||||
self.read_string()?;
|
||||
}
|
||||
OP_PROP_INT => { self.read_i64()?; }
|
||||
OP_PROP_FLOAT => { self.read_f64()?; }
|
||||
OP_PROP_BOOL => { self.read_byte()?; }
|
||||
OP_PROP_NULL => {}
|
||||
OP_PROP_ARRAY => {
|
||||
let count = self.read_u32()?;
|
||||
for _ in 0..count {
|
||||
let elem_op = self.read_byte()?;
|
||||
self.skip_value(elem_op)?;
|
||||
}
|
||||
}
|
||||
OP_PROP_UNIT => {
|
||||
self.read_f64()?; // number
|
||||
self.read_string()?; // unit suffix
|
||||
}
|
||||
OP_PROP_CALL => {
|
||||
self.read_string()?; // function name
|
||||
let arg_count = self.read_u32()?;
|
||||
for _ in 0..arg_count {
|
||||
let op = self.read_byte()?;
|
||||
self.skip_value(op)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Skip a full opcode + its payload without interpreting it.
|
||||
pub fn skip_opcode(&mut self, op: u8) -> Result<(), InterpError> {
|
||||
match op {
|
||||
OP_VERSION => { self.read_i64()?; }
|
||||
OP_STYLE | OP_RHEI_BLK => { self.read_string()?; }
|
||||
|
||||
OP_PROP_STR
|
||||
| OP_PROP_COLOR
|
||||
| OP_PROP_FSPATH
|
||||
| OP_PROP_VAR
|
||||
| OP_PROP_RHEI
|
||||
| OP_PROP_INT
|
||||
| OP_PROP_FLOAT
|
||||
| OP_PROP_BOOL
|
||||
| OP_PROP_NULL
|
||||
| OP_PROP_ARRAY
|
||||
| OP_PROP_CALL
|
||||
| OP_PROP_UNIT
|
||||
| OP_PROP_IDENT => {
|
||||
self.read_string()?; // key
|
||||
self.skip_value(op)?; // value
|
||||
}
|
||||
|
||||
OP_GLOBAL | OP_LET => {
|
||||
self.read_string()?;
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
}
|
||||
OP_SINGLETON => {
|
||||
self.read_string()?;
|
||||
let count = self.read_u32()?;
|
||||
for _ in 0..count {
|
||||
self.read_string()?;
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
}
|
||||
}
|
||||
OP_COMPONENT => {
|
||||
self.read_string()?;
|
||||
let params = self.read_u32()?;
|
||||
for _ in 0..params {
|
||||
self.read_string()?;
|
||||
self.read_string()?;
|
||||
}
|
||||
self.skip_block()?;
|
||||
}
|
||||
OP_IF => {
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
self.skip_block()?;
|
||||
if self.read_byte()? == 1 { self.skip_block()?; }
|
||||
}
|
||||
OP_EACH => {
|
||||
self.read_string()?;
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
self.skip_block()?;
|
||||
}
|
||||
OP_ON => {
|
||||
self.read_string()?;
|
||||
let args = self.read_u32()?;
|
||||
for _ in 0..args {
|
||||
self.read_string()?;
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
}
|
||||
self.skip_block()?;
|
||||
}
|
||||
OP_ELEM_PUSH => { self.read_string()?; }
|
||||
OP_CONTENT => {
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
}
|
||||
|
||||
OP_STYLE_RULE => {
|
||||
self.read_string()?; // selector
|
||||
let count = self.read_u32()?;
|
||||
for _ in 0..count {
|
||||
self.read_string()?; // property key
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?; // property value
|
||||
}
|
||||
}
|
||||
OP_STYLE_ANIM => {
|
||||
self.read_string()?; // animation name
|
||||
let frame_count = self.read_u32()?;
|
||||
for _ in 0..frame_count {
|
||||
self.read_string()?; // step ("from", "to", "50%", …)
|
||||
let prop_count = self.read_u32()?;
|
||||
for _ in 0..prop_count {
|
||||
self.read_string()?;
|
||||
let vop = self.read_byte()?;
|
||||
self.skip_value(vop)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn skip_block(&mut self) -> Result<(), InterpError> {
|
||||
loop {
|
||||
let op = self.read_byte()?;
|
||||
if op == OP_END_BLOCK { return Ok(()); }
|
||||
self.skip_opcode(op)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[inline]
|
||||
fn require(&self, n: usize) -> Result<(), InterpError> {
|
||||
if self.remaining() < n {
|
||||
Err(InterpError::UnexpectedEof)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
169
src/interpreter/rhei.rs
Normal file
169
src/interpreter/rhei.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use rhai::{Dynamic, Engine, Scope, AST};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const RHEI_PREFIX: &str = "__rhei:";
|
||||
|
||||
pub struct RheiContext {
|
||||
engine: Engine,
|
||||
init_ast: AST,
|
||||
fn_ast: AST,
|
||||
}
|
||||
|
||||
impl RheiContext {
|
||||
pub fn new(scripts: &[String]) -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine.on_print(|s| println!("[rhei] {s}"));
|
||||
engine.on_debug(|s, src, pos| {
|
||||
eprintln!("[rhei debug @ {src:?}:{pos}] {s}");
|
||||
});
|
||||
|
||||
let mut combined = AST::empty();
|
||||
for (i, script) in scripts.iter().enumerate() {
|
||||
match engine.compile(script) {
|
||||
Ok(ast) => combined = combined.merge(&ast),
|
||||
Err(e) => eprintln!("⚠️ Rhei compile error (block #{i}): {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut fn_ast = combined.clone();
|
||||
fn_ast.clear_statements();
|
||||
|
||||
Self {
|
||||
engine,
|
||||
init_ast: combined,
|
||||
fn_ast
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
|
||||
let mut scope = Scope::new();
|
||||
for (k, v) in variables.iter() {
|
||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||
}
|
||||
|
||||
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) {
|
||||
eprintln!("⚠️ Rhei init error: {e}");
|
||||
}
|
||||
|
||||
let names: Vec<String> = scope.iter_raw()
|
||||
.map(|(name, _, _)| name.to_string())
|
||||
.collect();
|
||||
for name in &names {
|
||||
if let Some(val) = scope.get_value::<Dynamic>(name) {
|
||||
variables.insert(name.clone(), dyn_to_str(&val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> Option<String> {
|
||||
let mut scope = Scope::new();
|
||||
for (k, v) in variables {
|
||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||
}
|
||||
|
||||
let expr_ast = self.engine
|
||||
.compile_expression(expr)
|
||||
.or_else(|_| self.engine.compile(expr))
|
||||
.ok()?;
|
||||
|
||||
let full = self.fn_ast.merge(&expr_ast);
|
||||
|
||||
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
|
||||
Ok(val) => Some(dyn_to_str(&val)),
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei eval `{expr}`: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
|
||||
let mut scope = Scope::new();
|
||||
for (k, v) in variables {
|
||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||
}
|
||||
|
||||
let ast = match self.engine.compile_expression(expr) {
|
||||
Ok(a) => a,
|
||||
Err(_) => match self.engine.compile(expr) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei condition compile `{expr}`: {e}");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let full = self.fn_ast.merge(&ast);
|
||||
|
||||
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
|
||||
Ok(val) => {
|
||||
if val.is_bool() { return val.cast::<bool>(); }
|
||||
if val.is_int() { return val.cast::<i64>() != 0; }
|
||||
if val.is_float(){ return val.cast::<f64>() != 0.0; }
|
||||
if val.is_string(){
|
||||
let s = val.cast::<String>();
|
||||
return !matches!(s.trim(), "" | "false" | "0" | "null");
|
||||
}
|
||||
!val.is_unit()
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
|
||||
let mut scope = Scope::new();
|
||||
for (k, v) in variables.iter() {
|
||||
scope.push_dynamic(k.clone(), str_to_dyn(v));
|
||||
}
|
||||
|
||||
match self.engine.compile(script) {
|
||||
Ok(action_ast) => {
|
||||
let full = self.fn_ast.merge(&action_ast);
|
||||
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) {
|
||||
eprintln!("⚠️ Rhei action execution error: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("⚠️ Rhei action compilation error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
let names: Vec<String> = scope.iter_raw()
|
||||
.map(|(name, _, _)| name.to_string())
|
||||
.collect();
|
||||
for name in &names {
|
||||
if let Some(val) = scope.get_value::<Dynamic>(name) {
|
||||
variables.insert(name.clone(), dyn_to_str(&val));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RheiContext {
|
||||
fn default() -> Self {
|
||||
Self::new(&[])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn str_to_dyn(s: &str) -> Dynamic {
|
||||
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); }
|
||||
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); }
|
||||
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
|
||||
Dynamic::from(s.to_owned())
|
||||
}
|
||||
|
||||
pub fn dyn_to_str(val: &Dynamic) -> String {
|
||||
if val.is_string() {
|
||||
return val.clone().cast::<String>();
|
||||
}
|
||||
if val.is_unit() {
|
||||
return String::new();
|
||||
}
|
||||
val.to_string()
|
||||
}
|
||||
183
src/interpreter/style.rs
Normal file
183
src/interpreter/style.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StyleSheet {
|
||||
index: HashMap<String, HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl StyleSheet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
|
||||
self.index
|
||||
.entry(selector.trim().to_string())
|
||||
.or_default()
|
||||
.extend(properties);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> {
|
||||
self.index.get(element_type.trim())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.index.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ComputedStyle {
|
||||
pub font_size: Option<f32>,
|
||||
pub color: Option<iced::Color>,
|
||||
pub padding: Option<f32>,
|
||||
pub background: Option<iced::Color>,
|
||||
pub spacing: Option<f32>,
|
||||
pub border_radius: Option<f32>,
|
||||
pub border_width: Option<f32>,
|
||||
pub border_color: Option<iced::Color>,
|
||||
pub width: Option<f32>,
|
||||
pub direction: Option<LayoutDirection>,
|
||||
pub align_items: Option<iced::Alignment>,
|
||||
pub content_align: Option<ContentAlign>,
|
||||
}
|
||||
|
||||
impl ComputedStyle {
|
||||
pub fn compute(
|
||||
inline: &HashMap<String, String>,
|
||||
sheet: Option<&HashMap<String, String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
font_size: lookup("font-size", inline, sheet).and_then(parse_size),
|
||||
color: lookup("color", inline, sheet).and_then(parse_color),
|
||||
padding: lookup("padding", inline, sheet).and_then(parse_size),
|
||||
background: lookup("background", inline, sheet)
|
||||
.or_else(|| lookup("background-color", inline, sheet))
|
||||
.and_then(parse_color),
|
||||
spacing: lookup("spacing", inline, sheet)
|
||||
.or_else(|| lookup("gap", inline, sheet))
|
||||
.and_then(parse_size),
|
||||
border_radius: lookup("border-radius", inline, sheet).and_then(parse_size),
|
||||
border_width: lookup("border-width", inline, sheet).and_then(parse_size),
|
||||
border_color: lookup("border-color", inline, sheet).and_then(parse_color),
|
||||
width: lookup("width", inline, sheet).and_then(parse_size),
|
||||
|
||||
direction: lookup("direction", inline, sheet).and_then(parse_direction),
|
||||
align_items: lookup("align-items", inline, sheet).and_then(parse_alignment),
|
||||
content_align: lookup("content-align", inline, sheet).and_then(parse_content_align),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn lookup<'a>(
|
||||
key: &str,
|
||||
inline: &'a HashMap<String, String>,
|
||||
sheet: Option<&'a HashMap<String, String>>,
|
||||
) -> Option<&'a str> {
|
||||
if let Some(v) = inline.get(key) {
|
||||
return Some(v.as_str());
|
||||
}
|
||||
if let Some(v) = inline.get(&format!("style:{key}")) {
|
||||
return Some(v.as_str());
|
||||
}
|
||||
sheet.and_then(|s| s.get(key)).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn parse_color(s: &str) -> Option<iced::Color> {
|
||||
let s = s.trim();
|
||||
|
||||
if let Some(hex) = s.strip_prefix('#') {
|
||||
return match hex.len() {
|
||||
3 => {
|
||||
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
|
||||
Some(iced::Color::from_rgb(
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
))
|
||||
}
|
||||
6 => {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
Some(iced::Color::from_rgb(
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
))
|
||||
}
|
||||
8 => {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
|
||||
Some(iced::Color::from_rgba(
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
a as f32 / 255.0,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
match s {
|
||||
"white" => Some(iced::Color::WHITE),
|
||||
"black" => Some(iced::Color::BLACK),
|
||||
"transparent" => Some(iced::Color::TRANSPARENT),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LayoutDirection {
|
||||
Column,
|
||||
Row,
|
||||
Grid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContentAlign {
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"horizontal" | "row" => Some(LayoutDirection::Row),
|
||||
"vertical" | "column" => Some(LayoutDirection::Column),
|
||||
"grid" => Some(LayoutDirection::Grid),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"start" => Some(iced::Alignment::Start),
|
||||
"center" => Some(iced::Alignment::Center),
|
||||
"end" => Some(iced::Alignment::End),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"start" | "left" | "top" => Some(ContentAlign::Start),
|
||||
"center" => Some(ContentAlign::Center),
|
||||
"end" | "right" | "bottom" => Some(ContentAlign::End),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_size(s: &str) -> Option<f32> {
|
||||
s.trim()
|
||||
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%')
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
}
|
||||
60
src/interpreter/types.rs
Normal file
60
src/interpreter/types.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
use super::style::{ComputedStyle, StyleSheet};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element {
|
||||
pub type_name: String,
|
||||
pub properties: HashMap<String, String>,
|
||||
pub children: Vec<Element>,
|
||||
pub computed_style: ComputedStyle,
|
||||
}
|
||||
|
||||
impl Element {
|
||||
pub fn new(type_name: String) -> Self {
|
||||
Self {
|
||||
type_name,
|
||||
properties: HashMap::new(),
|
||||
children: Vec::new(),
|
||||
computed_style: ComputedStyle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComponentDef {
|
||||
pub name: String,
|
||||
pub params: Vec<(String, String)>,
|
||||
pub children: Vec<Element>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Document {
|
||||
pub roots: Vec<Element>,
|
||||
pub components: HashMap<String, ComponentDef>,
|
||||
pub variables: HashMap<String, String>,
|
||||
pub rhei_scripts: Vec<String>,
|
||||
pub stylesheet: StyleSheet,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InterpError {
|
||||
BadMagic,
|
||||
UnexpectedEof,
|
||||
InvalidUtf8,
|
||||
UnexpectedPop,
|
||||
}
|
||||
|
||||
impl fmt::Display for InterpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::BadMagic => write!(f, "Bad magic bytes — not a .glbc file"),
|
||||
Self::UnexpectedEof => write!(f, "Unexpected end of bytecode"),
|
||||
Self::InvalidUtf8 => write!(f, "String is not valid UTF-8"),
|
||||
Self::UnexpectedPop => write!(f, "OP_ELEM_POP without matching OP_ELEM_PUSH"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InterpError {}
|
||||
64
src/main.rs
Normal file
64
src/main.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
mod app;
|
||||
mod cli;
|
||||
mod interpreter;
|
||||
mod renderer;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
EventTriggered(String),
|
||||
InputChanged(Option<String>, String),
|
||||
ToggleChanged(Option<String>, bool),
|
||||
SliderChanged(Option<String>, f64),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "glint-runtime")]
|
||||
#[command(author, version, about = "Glint runtime – compile and run .gltm/.glts files", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Compile {
|
||||
#[arg(required = true)]
|
||||
gltm_files: Vec<String>,
|
||||
|
||||
#[arg(short = 's', long = "style")]
|
||||
glts_files: Vec<String>,
|
||||
|
||||
#[arg(short = 'o', long = "output")]
|
||||
output: Option<String>,
|
||||
},
|
||||
|
||||
Run {
|
||||
/// Bytecode file to execute
|
||||
file: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Compile {
|
||||
gltm_files,
|
||||
glts_files,
|
||||
output,
|
||||
} => {
|
||||
let output_path = output.unwrap_or_else(|| {
|
||||
std::path::Path::new(&gltm_files[0])
|
||||
.with_extension("glbc")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
cli::compile_files(&gltm_files, &glts_files, &output_path);
|
||||
}
|
||||
Commands::Run { file } => {
|
||||
cli::run_file(&file);
|
||||
}
|
||||
}
|
||||
}
|
||||
350
src/renderer.rs
Normal file
350
src/renderer.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, progress_bar, row, slider, text, text_input, image, svg
|
||||
};
|
||||
use iced::{Alignment, Background, Border, Length, Theme};
|
||||
use iced::widget::container::Style as ContainerStyle;
|
||||
use crate::interpreter::style::{LayoutDirection, ContentAlign, ComputedStyle};
|
||||
use crate::interpreter::Element;
|
||||
use crate::Message;
|
||||
|
||||
fn extract_var_binding(el: &Element, prop: &str) -> Option<String> {
|
||||
el.properties.get(&format!("__bind:{prop}")).cloned()
|
||||
}
|
||||
|
||||
fn render_children<'a>(
|
||||
children: &'a [Element],
|
||||
) -> Vec<iced::Element<'a, Message, Theme, iced::Renderer>> {
|
||||
children.iter().filter_map(render_element).collect()
|
||||
}
|
||||
|
||||
fn get_button_style(cs: &ComputedStyle, status: button::Status) -> button::Style {
|
||||
let base_bg = cs.background.unwrap_or(iced::Color::from_rgb(0.25, 0.26, 0.35));
|
||||
|
||||
let bg_color = match status {
|
||||
button::Status::Hovered => iced::Color {
|
||||
r: (base_bg.r * 1.15).min(1.0),
|
||||
g: (base_bg.g * 1.15).min(1.0),
|
||||
b: (base_bg.b * 1.15).min(1.0),
|
||||
a: base_bg.a,
|
||||
},
|
||||
button::Status::Pressed => iced::Color {
|
||||
r: base_bg.r * 0.85,
|
||||
g: base_bg.g * 0.85,
|
||||
b: base_bg.b * 0.85,
|
||||
a: base_bg.a,
|
||||
},
|
||||
_ => base_bg,
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg_color)),
|
||||
border: Border {
|
||||
color: cs.border_color.unwrap_or(iced::Color::TRANSPARENT),
|
||||
width: cs.border_width.unwrap_or(0.0),
|
||||
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||
},
|
||||
text_color: cs.color.unwrap_or(iced::Color::WHITE),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text_input_style(cs: &ComputedStyle) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(cs.background.unwrap_or(iced::Color::from_rgb(0.12, 0.12, 0.14))),
|
||||
border: Border {
|
||||
color: cs.border_color.unwrap_or(iced::Color::from_rgb(0.25, 0.25, 0.3)),
|
||||
width: cs.border_width.unwrap_or(1.0),
|
||||
radius: cs.border_radius.unwrap_or(0.0).into(),
|
||||
},
|
||||
icon: iced::Color::from_rgb(0.5, 0.5, 0.5),
|
||||
placeholder: iced::Color::from_rgb(0.4, 0.4, 0.45),
|
||||
value: cs.color.unwrap_or(iced::Color::WHITE),
|
||||
selection: iced::Color::from_rgb(0.3, 0.3, 0.5),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn apply_container_style<'a>(
|
||||
c: container::Container<'a, Message, Theme, iced::Renderer>,
|
||||
cs: &ComputedStyle,
|
||||
) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let bg = cs.background;
|
||||
let radius = cs.border_radius.unwrap_or(0.0);
|
||||
let bwidth = cs.border_width.unwrap_or(0.0);
|
||||
let bcolor = cs.border_color.unwrap_or(iced::Color::TRANSPARENT);
|
||||
|
||||
let needs_style = bg.is_some() || radius != 0.0 || bwidth != 0.0;
|
||||
|
||||
let c = if needs_style {
|
||||
c.style(move |_theme| ContainerStyle {
|
||||
background: bg.map(Background::Color),
|
||||
border: Border {
|
||||
color: bcolor,
|
||||
width: bwidth,
|
||||
radius: radius.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
c.style(|_theme| ContainerStyle::default())
|
||||
};
|
||||
|
||||
match cs.width {
|
||||
Some(w) => c.width(Length::Fixed(w)).into(),
|
||||
None => c.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_element<'a>(
|
||||
el: &'a Element,
|
||||
) -> Option<iced::Element<'a, Message, Theme, iced::Renderer>> {
|
||||
let props = &el.properties;
|
||||
let cs = &el.computed_style;
|
||||
|
||||
match el.type_name.as_str() {
|
||||
"Window" => {
|
||||
let padding = cs.padding.unwrap_or(0.0);
|
||||
let spacing = cs.spacing.unwrap_or(12.0);
|
||||
let mut col = column![].spacing(spacing);
|
||||
for child in render_children(&el.children) { col = col.push(child); }
|
||||
Some(apply_container_style(container(col).padding(padding), cs))
|
||||
}
|
||||
|
||||
"Panel" => Some(render_panel(el)),
|
||||
|
||||
"Title" | "Header" => {
|
||||
let content = props.get("text").cloned().unwrap_or_else(|| "Title".into());
|
||||
let mut widget = text(content).size(cs.font_size.unwrap_or(24.0));
|
||||
if let Some(col) = cs.color { widget = widget.color(col); }
|
||||
Some(widget.into())
|
||||
}
|
||||
|
||||
"Text" | "Label" | "#text" => {
|
||||
let content = props.get("text").cloned().unwrap_or_default();
|
||||
let mut widget = text(content).size(cs.font_size.unwrap_or(16.0));
|
||||
if let Some(col) = cs.color { widget = widget.color(col); }
|
||||
Some(widget.into())
|
||||
}
|
||||
|
||||
"Image" => {
|
||||
let src = props.get("src").cloned().unwrap_or_default();
|
||||
|
||||
let path = src.strip_prefix("fs:").unwrap_or(&src);
|
||||
|
||||
let mut element: iced::Element<'a, Message, Theme, iced::Renderer> = if path.ends_with(".svg") {
|
||||
let mut svg_widget = svg(svg::Handle::from_path(path));
|
||||
if let Some(w) = cs.width {
|
||||
svg_widget = svg_widget.width(Length::Fixed(w));
|
||||
} else {
|
||||
svg_widget = svg_widget.width(Length::Shrink);
|
||||
}
|
||||
svg_widget.into()
|
||||
} else {
|
||||
let mut img_widget = image(path);
|
||||
if let Some(w) = cs.width {
|
||||
img_widget = img_widget.width(Length::Fixed(w));
|
||||
} else {
|
||||
img_widget = img_widget.width(Length::Shrink);
|
||||
}
|
||||
img_widget.into()
|
||||
};
|
||||
|
||||
if cs.background.is_some() || cs.border_radius.is_some() || cs.border_width.is_some() {
|
||||
Some(apply_container_style(container(element), cs))
|
||||
} else {
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
|
||||
"Button" => Some(render_button(el)),
|
||||
"Input" => Some(render_input(el)),
|
||||
"Toggle" => Some(render_toggle(el)),
|
||||
"Slider" => Some(render_slider(el)),
|
||||
|
||||
"Icon" => Some(text("🔹").size(18).into()),
|
||||
|
||||
"ProgressBar" => {
|
||||
let value = props.get("value")
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.unwrap_or(0.0);
|
||||
Some(progress_bar(0.0..=100.0, value).into())
|
||||
}
|
||||
|
||||
"Divider" | "Separator" => Some(iced::widget::rule::horizontal(1).into()),
|
||||
|
||||
_ => {
|
||||
if el.children.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let padding = cs.padding.unwrap_or(0.0);
|
||||
let spacing = cs.spacing.unwrap_or(10.0);
|
||||
let mut col = column![].spacing(spacing);
|
||||
for child in render_children(&el.children) { col = col.push(child); }
|
||||
|
||||
if cs.background.is_none() && cs.border_radius.is_none() && cs.border_width.is_none() {
|
||||
Some(col.into())
|
||||
} else {
|
||||
Some(apply_container_style(container(col).padding(padding), cs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_panel<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let cs = &el.computed_style;
|
||||
let padding = cs.padding.unwrap_or(5.0);
|
||||
let spacing = cs.spacing.unwrap_or(10.0);
|
||||
|
||||
let is_horizontal = el.children.iter().all(|c| {
|
||||
matches!(
|
||||
c.type_name.as_str(),
|
||||
"Icon" | "Button" | "MenuItem" | "Label" | "Toggle" | "Text" | "#text"
|
||||
)
|
||||
});
|
||||
|
||||
let direction = cs.direction.unwrap_or(if is_horizontal {
|
||||
LayoutDirection::Row
|
||||
} else {
|
||||
LayoutDirection::Column
|
||||
});
|
||||
|
||||
let content: iced::Element<'a, Message, Theme, iced::Renderer> = match direction {
|
||||
LayoutDirection::Row => {
|
||||
let mut r = row![].spacing(spacing);
|
||||
if let Some(align) = cs.align_items {
|
||||
r = r.align_y(align);
|
||||
} else {
|
||||
r = r.align_y(Alignment::Center);
|
||||
}
|
||||
for child in render_children(&el.children) { r = r.push(child); }
|
||||
r.into()
|
||||
}
|
||||
LayoutDirection::Column => {
|
||||
let mut c = column![].spacing(spacing);
|
||||
if let Some(align) = cs.align_items {
|
||||
c = c.align_x(align);
|
||||
}
|
||||
for child in render_children(&el.children) { c = c.push(child); }
|
||||
c.into()
|
||||
}
|
||||
LayoutDirection::Grid => {
|
||||
let mut c = column![].spacing(spacing);
|
||||
let cols = el.properties.get("columns")
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(3);
|
||||
|
||||
for chunk in el.children.chunks(cols) {
|
||||
let mut r = row![].spacing(spacing);
|
||||
if let Some(align) = cs.align_items {
|
||||
r = r.align_y(align);
|
||||
} else {
|
||||
r = r.align_y(Alignment::Center);
|
||||
}
|
||||
for child in render_children(chunk) { r = r.push(child); }
|
||||
c = c.push(r);
|
||||
}
|
||||
c.into()
|
||||
}
|
||||
};
|
||||
|
||||
let mut cont = container(content).padding(padding);
|
||||
|
||||
if let Some(ca) = cs.content_align {
|
||||
match ca {
|
||||
ContentAlign::Start => cont = cont.align_x(iced::alignment::Horizontal::Left),
|
||||
ContentAlign::Center => cont = cont.align_x(iced::alignment::Horizontal::Center),
|
||||
ContentAlign::End => cont = cont.align_x(iced::alignment::Horizontal::Right),
|
||||
}
|
||||
cont = cont.width(Length::Fill);
|
||||
}
|
||||
|
||||
apply_container_style(cont, cs)
|
||||
}
|
||||
|
||||
fn render_button<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let props = &el.properties;
|
||||
let cs = &el.computed_style;
|
||||
|
||||
let content: iced::Element<'a, _, _, _> = if el.children.is_empty() {
|
||||
let label = props.get("label").cloned().unwrap_or_else(|| "Button".into());
|
||||
let mut t = text(label).size(cs.font_size.unwrap_or(14.0));
|
||||
if let Some(col) = cs.color { t = t.color(col); }
|
||||
t.into()
|
||||
} else {
|
||||
let mut r = row![].spacing(8);
|
||||
for child in render_children(&el.children) { r = r.push(child); }
|
||||
r.into()
|
||||
};
|
||||
|
||||
let padding = cs.padding
|
||||
.map(|p| [p, p * 2.0])
|
||||
.unwrap_or([8.0, 16.0]);
|
||||
|
||||
let mut btn = button(content).padding(padding);
|
||||
|
||||
if let Some(script) = props.get("__on:click") {
|
||||
btn = btn.on_press(Message::EventTriggered(script.clone()));
|
||||
} else {
|
||||
btn = btn.on_press(Message::EventTriggered(String::new()));
|
||||
}
|
||||
|
||||
if let Some(w) = cs.width {
|
||||
btn = btn.width(Length::Fixed(w));
|
||||
}
|
||||
|
||||
let cs_clone = *cs;
|
||||
let btn = btn.style(move |_, status| get_button_style(&cs_clone, status));
|
||||
|
||||
btn.into()
|
||||
}
|
||||
|
||||
fn render_input<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let props = &el.properties;
|
||||
let placeholder = props.get("placeholder")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Type here…".into());
|
||||
let value = props.get("value").cloned().unwrap_or_default();
|
||||
let var_name = extract_var_binding(el, "value");
|
||||
let padding = el.computed_style.padding.unwrap_or(10.0);
|
||||
let cs = &el.computed_style;
|
||||
|
||||
let mut input = text_input(&placeholder, &value)
|
||||
.on_input(move |v| Message::InputChanged(var_name.clone(), v))
|
||||
.padding(padding);
|
||||
|
||||
if let Some(w) = cs.width {
|
||||
input = input.width(Length::Fixed(w));
|
||||
}
|
||||
|
||||
let cs_clone = *cs;
|
||||
input = input.style(move |_, _| get_text_input_style(&cs_clone));
|
||||
|
||||
input.into()
|
||||
}
|
||||
|
||||
fn render_toggle<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let props = &el.properties;
|
||||
let label = props.get("label").cloned().unwrap_or_default();
|
||||
let value = props.get("value")
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
let var_name = extract_var_binding(el, "value");
|
||||
|
||||
checkbox(value)
|
||||
.label(label)
|
||||
.on_toggle(move |v| Message::ToggleChanged(var_name.clone(), v))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_slider<'a>(el: &'a Element) -> iced::Element<'a, Message, Theme, iced::Renderer> {
|
||||
let props = &el.properties;
|
||||
let value = props.get("value")
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(0.0);
|
||||
let var_name = extract_var_binding(el, "value");
|
||||
|
||||
slider(0.0..=100.0, value, move |v| Message::SliderChanged(var_name.clone(), v))
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user