Compare commits

...

14 Commits

Author SHA1 Message Date
Faynot
04b354d85c fix: version 2026-06-02 13:05:16 +03:00
Faynot
cedc2d1bc0 fix: version 2026-06-02 13:05:02 +03:00
Faynot
d82750081a feat: add styles 2026-06-02 13:03:23 +03:00
Faynot
4b5a340351 fix: versions 2026-05-18 18:31:55 +03:00
Faynot
e01490774d fix: parse & compile, add null & array support 2026-05-18 18:30:04 +03:00
Faynot
4d91abc9ce fix: update version 2026-05-18 15:00:32 +03:00
Faynot
c530ef26e4 fix: update version 2026-05-18 15:00:09 +03:00
Faynot
6331769f2a fix: remov [bin] from Cargo.toml 2026-05-18 14:59:38 +03:00
Faynot
6b8192b9b2 update readme.md 2026-05-18 14:55:04 +03:00
Faynot
e6ed1bd9fd update readme.md 2026-05-18 14:54:16 +03:00
Faynot
9696f8f4ba fix: Cargo.toml 2026-05-18 14:53:16 +03:00
Faynot
989a84bea7 refactor: Cargo.toml 2026-05-18 14:51:48 +03:00
Faynot
9477f258e5 Merge branch 'main' of git.inotfail.com:INotFail/Glint 2026-05-18 14:39:21 +03:00
Faynot
355844980a refactor: move project to lib crate 2026-05-18 14:38:31 +03:00
11 changed files with 976 additions and 645 deletions

4
Cargo.lock generated
View File

@@ -3,5 +3,5 @@
version = 4 version = 4
[[package]] [[package]]
name = "glint" name = "glt"
version = "0.1.0" version = "0.1.4"

View File

@@ -1,6 +1,16 @@
[package] [package]
name = "glint" name = "glt"
version = "0.1.0" version = "0.1.4"
edition = "2024" edition = "2024"
description = "Glint compiler library"
license = "MIT"
repository = "https://git.inotfail.com/INotFail/Glint"
readme = "README.md"
keywords = ["ui", "compiler"]
categories = ["compilers"]
[lib]
name = "glt"
path = "src/lib.rs"
[dependencies] [dependencies]

View File

@@ -1,4 +1,4 @@
<h1 align="center"><img alt="DumbShot" src="./logo.png"></h1> <h1 align="center"><img alt="Glint" src="./logo.png"></h1>
<p align="center">A specialized, statically typed markup, styling, and state management language created exclusively for <strong>Eclipse DE</strong></p> <p align="center">A specialized, statically typed markup, styling, and state management language created exclusively for <strong>Eclipse DE</strong></p>
## About Glint ## About Glint
@@ -14,11 +14,13 @@ Eclipse doesn't have a concept of "default settings" in the form of hidden binar
- **Who is Glint for?**: Knowing Glint is necessary for advanced users who want full control over their DE, however, no one forbids the use of the unmodified Eclipse DE - **Who is Glint for?**: Knowing Glint is necessary for advanced users who want full control over their DE, however, no one forbids the use of the unmodified Eclipse DE
## Architecture ## Architecture
The language is designed with a focus on maximum predictability, determinism, and performance of the native application layer on 'iced' (Rust). The language is designed with a focus on maximum predictability, determinism, and performance of the native application layer on 'iced' (Rust).
### Sigil system ### Sigil system
| Symbol | Type | Description | | Symbol | Type | Description |
| --- | --- | --- | | -------------- | --------- | ---------------------------------------------------------- |
| `@` | Directive | Single symbol → instant dispatch | | `@` | Directive | Single symbol → instant dispatch |
| `$` | Varialbe | Distinguishes from item names and strings | | `$` | Varialbe | Distinguishes from item names and strings |
| `!rhei` | call Rhei | '!' does not occur in names; The body is transmitted as-is | | `!rhei` | call Rhei | '!' does not occur in names; The body is transmitted as-is |
@@ -32,11 +34,14 @@ The language is designed with a focus on maximum predictability, determinism, an
The parser spends **zero time** determining the type of token. It doesn't need to symbolically compare strings like `if`, `let`, or `function`. The parser spends **zero time** determining the type of token. It doesn't need to symbolically compare strings like `if`, `let`, or `function`.
### 2. Separation of scopes '()' and '{}' ### 2. Separation of scopes '()' and '{}'
* Parentheses '(...)' contain **only** the properties of the element in the format 'key=value'.
* Curly braces '{...}' contain **only** child elements and directives. - Parentheses '(...)' contain **only** the properties of the element in the format 'key=value'.
- Curly braces '{...}' contain **only** child elements and directives.
### 3. Native protocols ### 3. Native protocols
Instead of calling heavy functions like 'readFile("/path")' that require argument parsing in runtime, Glint uses built-in prefixes: Instead of calling heavy functions like 'readFile("/path")' that require argument parsing in runtime, Glint uses built-in prefixes:
```gltm ```gltm
Image(src=fs:/usr/share/backgrounds/wallpaper.png) Image(src=fs:/usr/share/backgrounds/wallpaper.png)
``` ```
@@ -48,6 +53,7 @@ Glint does not interpret pure text while DE is running. It compiles it into a co
This allows the Render Hot-Path to run without memory allocations at all This allows the Render Hot-Path to run without memory allocations at all
## Syntax example ## Syntax example
```gltm ```gltm
@version 1 @version 1
@style "desktop.glts" @style "desktop.glts"

View File

@@ -16,6 +16,12 @@ pub enum Value {
FsPath(String), FsPath(String),
Variable(String), Variable(String),
Rhei(String), Rhei(String),
Null,
Array(Vec<Value>),
// glts
Call(String, Vec<Value>), // rgba(30, 30, 46, 0.88)
Unit(f64, String), // 8px, 120ms
Ident(String),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -55,6 +61,14 @@ pub enum Directive {
child_span: (u32, u32), child_span: (u32, u32),
}, },
RheiBlock(String), RheiBlock(String),
StyleRule {
selector: String,
prop_span: (u32, u32),
},
StyleAnim {
name: String,
frames: Vec<(String, (u32, u32))>,
},
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]

View File

@@ -10,23 +10,22 @@ impl<'a> Compiler<'a> {
pub fn new(module: &'a ModuleSoA) -> Self { pub fn new(module: &'a ModuleSoA) -> Self {
Self { Self {
module, module,
buf: Vec::new(), buf: Vec::with_capacity(module.hierarchy.len() * 32 + std::mem::size_of_val(&MAGIC_HEADER)),
} }
} }
pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec<u8> { pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec<u8> {
self.buf.extend_from_slice(&MAGIC_HEADER); self.buf.extend_from_slice(&MAGIC_HEADER);
self.compile_span(root_nodes); self.compile_span(root_nodes);
self.buf self.buf
} }
#[inline]
fn compile_span(&mut self, nodes: &[NodeId]) { fn compile_span(&mut self, nodes: &[NodeId]) {
for node in nodes { for &node in nodes {
match node { match node {
NodeId::Element(id) => self.compile_element(*id), NodeId::Element(id) => self.compile_element(id),
NodeId::Directive(id) => self.compile_directive(*id), NodeId::Directive(id) => self.compile_directive(id),
} }
} }
} }
@@ -39,18 +38,17 @@ impl<'a> Compiler<'a> {
self.buf.push(OP_END_BLOCK); self.buf.push(OP_END_BLOCK);
} }
fn compile_element(&mut self, id: u32) { fn compile_element(&mut self, id: u32) {
let idx = id as usize; let idx = id as usize;
let typ = &self.module.elem_types[idx];
self.buf.push(OP_ELEM_PUSH); self.buf.push(OP_ELEM_PUSH);
self.write_string(typ); self.write_string(&self.module.elem_types[idx]);
let (p_start, p_len) = self.module.elem_prop_spans[idx]; let (p_start, p_len) = self.module.elem_prop_spans[idx];
for i in p_start..(p_start + p_len) { let p_start = p_start as usize;
let key = &self.module.prop_keys[i as usize]; let p_end = p_start + p_len as usize;
let val = &self.module.prop_values[i as usize];
self.compile_property(key, val); for i in p_start..p_end {
self.compile_property(&self.module.prop_keys[i], &self.module.prop_values[i]);
} }
if let Some(content) = &self.module.elem_content[idx] { if let Some(content) = &self.module.elem_content[idx] {
@@ -60,15 +58,17 @@ impl<'a> Compiler<'a> {
let child_span = self.module.elem_child_spans[idx]; let child_span = self.module.elem_child_spans[idx];
if child_span.1 > 0 { if child_span.1 > 0 {
self.compile_block(child_span); let start = child_span.0 as usize;
let len = child_span.1 as usize;
let nodes = &self.module.hierarchy[start..start + len];
self.compile_span(nodes);
} }
self.buf.push(OP_ELEM_POP); self.buf.push(OP_ELEM_POP);
} }
fn compile_directive(&mut self, id: u32) { fn compile_directive(&mut self, id: u32) {
let dir = &self.module.directives[id as usize]; match &self.module.directives[id as usize] {
match dir {
Directive::Version(v) => { Directive::Version(v) => {
self.buf.push(OP_VERSION); self.buf.push(OP_VERSION);
self.write_i64(*v); self.write_i64(*v);
@@ -87,19 +87,14 @@ impl<'a> Compiler<'a> {
self.write_string(name); self.write_string(name);
self.write_u32(prop_span.1); self.write_u32(prop_span.1);
let (start, len) = *prop_span; let start = prop_span.0 as usize;
for i in start..(start + len) { let end = start + prop_span.1 as usize;
let key = &self.module.prop_keys[i as usize]; for i in start..end {
let val = &self.module.prop_values[i as usize]; self.write_string(&self.module.prop_keys[i]);
self.write_string(key); self.compile_value(&self.module.prop_values[i]);
self.compile_value(val);
} }
} }
Directive::Component { Directive::Component { name, params, child_span } => {
name,
params,
child_span,
} => {
self.buf.push(OP_COMPONENT); self.buf.push(OP_COMPONENT);
self.write_string(name); self.write_string(name);
self.write_u32(params.len() as u32); self.write_u32(params.len() as u32);
@@ -114,36 +109,24 @@ impl<'a> Compiler<'a> {
self.write_string(name); self.write_string(name);
self.compile_value(value); self.compile_value(value);
} }
Directive::If { Directive::If { condition, child_span, else_span } => {
condition,
child_span,
else_span,
} => {
self.buf.push(OP_IF); self.buf.push(OP_IF);
self.compile_value(condition); self.compile_value(condition);
self.compile_block(*child_span); self.compile_block(*child_span);
if let Some(es) = else_span { if let Some(es) = else_span {
self.buf.push(1); // Has else self.buf.push(1);
self.compile_block(*es); self.compile_block(*es);
} else { } else {
self.buf.push(0); // No else self.buf.push(0);
} }
} }
Directive::Each { Directive::Each { item, collection, child_span } => {
item,
collection,
child_span,
} => {
self.buf.push(OP_EACH); self.buf.push(OP_EACH);
self.write_string(item); self.write_string(item);
self.compile_value(collection); self.compile_value(collection);
self.compile_block(*child_span); self.compile_block(*child_span);
} }
Directive::On { Directive::On { event, args, child_span } => {
event,
args,
child_span,
} => {
self.buf.push(OP_ON); self.buf.push(OP_ON);
self.write_string(event); self.write_string(event);
self.write_u32(args.len() as u32); self.write_u32(args.len() as u32);
@@ -157,105 +140,134 @@ impl<'a> Compiler<'a> {
self.buf.push(OP_RHEI_BLK); self.buf.push(OP_RHEI_BLK);
self.write_string(code); self.write_string(code);
} }
Directive::StyleRule { selector, prop_span } => {
self.buf.push(OP_STYLE_RULE);
self.write_string(selector);
self.write_u32(prop_span.1);
let start = prop_span.0 as usize;
let end = start + prop_span.1 as usize;
for i in start..end {
self.write_string(&self.module.prop_keys[i]);
self.compile_value(&self.module.prop_values[i]);
}
}
Directive::StyleAnim { name, frames } => {
self.buf.push(OP_STYLE_ANIM);
self.write_string(name);
self.write_u32(frames.len() as u32);
for (step, span) in frames {
self.write_string(step);
self.write_u32(span.1);
let start = span.0 as usize;
let end = start + span.1 as usize;
for i in start..end {
self.write_string(&self.module.prop_keys[i]);
self.compile_value(&self.module.prop_values[i]);
}
}
}
} }
} }
#[inline]
fn compile_property(&mut self, key: &str, val: &Value) { fn compile_property(&mut self, key: &str, val: &Value) {
let opcode = match val {
Value::String(_) => OP_PROP_STR,
Value::Int(_) => OP_PROP_INT,
Value::Float(_) => OP_PROP_FLOAT,
Value::Bool(_) => OP_PROP_BOOL,
Value::Color(_) => OP_PROP_COLOR,
Value::FsPath(_) => OP_PROP_FSPATH,
Value::Variable(_) => OP_PROP_VAR,
Value::Rhei(_) => OP_PROP_RHEI,
Value::Null => OP_PROP_NULL,
Value::Array(_) => OP_PROP_ARRAY,
// glts
Value::Call(_, _) => OP_PROP_CALL,
Value::Unit(_, _) => OP_PROP_UNIT,
Value::Ident(_) => OP_PROP_IDENT,
};
self.buf.push(opcode);
self.write_string(key);
match val { match val {
Value::String(s) => { Value::Null => {}
self.buf.push(OP_PROP_STR); _ => self.compile_value_data(val),
self.write_string(key);
self.write_string(s);
}
Value::Int(i) => {
self.buf.push(OP_PROP_INT);
self.write_string(key);
self.write_i64(*i);
}
Value::Float(f) => {
self.buf.push(OP_PROP_FLOAT);
self.write_string(key);
self.write_f64(*f);
}
Value::Bool(b) => {
self.buf.push(OP_PROP_BOOL);
self.write_string(key);
self.buf.push(if *b { 1 } else { 0 });
}
Value::Color(c) => {
self.buf.push(OP_PROP_COLOR);
self.write_string(key);
self.write_string(c);
}
Value::FsPath(p) => {
self.buf.push(OP_PROP_FSPATH);
self.write_string(key);
self.write_string(p);
}
Value::Variable(v) => {
self.buf.push(OP_PROP_VAR);
self.write_string(key);
self.write_string(v);
}
Value::Rhei(r) => {
self.buf.push(OP_PROP_RHEI);
self.write_string(key);
self.write_string(r);
}
} }
} }
#[inline]
fn compile_value(&mut self, val: &Value) { fn compile_value(&mut self, val: &Value) {
let opcode = match val {
Value::String(_) => OP_PROP_STR,
Value::Int(_) => OP_PROP_INT,
Value::Float(_) => OP_PROP_FLOAT,
Value::Bool(_) => OP_PROP_BOOL,
Value::Color(_) => OP_PROP_COLOR,
Value::FsPath(_) => OP_PROP_FSPATH,
Value::Variable(_) => OP_PROP_VAR,
Value::Rhei(_) => OP_PROP_RHEI,
Value::Null => OP_PROP_NULL,
Value::Array(_) => OP_PROP_ARRAY,
// glts
Value::Call(_, _) => OP_PROP_CALL,
Value::Unit(_, _) => OP_PROP_UNIT,
Value::Ident(_) => OP_PROP_IDENT,
};
self.buf.push(opcode);
self.compile_value_data(val);
}
#[inline]
fn compile_value_data(&mut self, val: &Value) {
match val { match val {
Value::String(s) => { Value::String(s) | Value::Color(s) | Value::FsPath(s) | Value::Variable(s) | Value::Rhei(s) => {
self.buf.push(OP_PROP_STR);
self.write_string(s); self.write_string(s);
} }
Value::Int(i) => { Value::Int(i) => self.write_i64(*i),
self.buf.push(OP_PROP_INT); Value::Float(f) => self.write_f64(*f),
self.write_i64(*i); Value::Bool(b) => self.buf.push(*b as u8),
Value::Null => {}
Value::Array(arr) => {
self.write_u32(arr.len() as u32);
for v in arr {
self.compile_value(v);
} }
Value::Float(f) => {
self.buf.push(OP_PROP_FLOAT);
self.write_f64(*f);
} }
Value::Bool(b) => { Value::Ident(s) => self.write_string(s),
self.buf.push(OP_PROP_BOOL); Value::Unit(num, unit) => {
self.buf.push(if *b { 1 } else { 0 }); self.write_f64(*num);
self.write_string(unit);
} }
Value::Color(c) => { Value::Call(name, args) => {
self.buf.push(OP_PROP_COLOR); self.write_string(name);
self.write_string(c); self.write_u32(args.len() as u32);
for arg in args {
self.compile_value(arg);
} }
Value::FsPath(p) => {
self.buf.push(OP_PROP_FSPATH);
self.write_string(p);
}
Value::Variable(v) => {
self.buf.push(OP_PROP_VAR);
self.write_string(v);
}
Value::Rhei(r) => {
self.buf.push(OP_PROP_RHEI);
self.write_string(r);
} }
} }
} }
#[inline(always)]
fn write_string(&mut self, s: &str) { fn write_string(&mut self, s: &str) {
let bytes = s.as_bytes(); let bytes = s.as_bytes();
self.write_u32(bytes.len() as u32); self.write_u32(bytes.len() as u32);
self.buf.extend_from_slice(bytes); self.buf.extend_from_slice(bytes);
} }
#[inline(always)]
fn write_u32(&mut self, v: u32) { fn write_u32(&mut self, v: u32) {
self.buf.extend_from_slice(&v.to_le_bytes()); self.buf.extend_from_slice(&v.to_le_bytes());
} }
#[inline(always)]
fn write_i64(&mut self, v: i64) { fn write_i64(&mut self, v: i64) {
self.buf.extend_from_slice(&v.to_le_bytes()); self.buf.extend_from_slice(&v.to_le_bytes());
} }
#[inline(always)]
fn write_f64(&mut self, v: f64) { fn write_f64(&mut self, v: f64) {
self.buf.extend_from_slice(&v.to_le_bytes()); self.buf.extend_from_slice(&v.to_le_bytes());
} }

25
src/lib.rs Normal file
View File

@@ -0,0 +1,25 @@
pub mod ast;
pub mod compiler;
pub mod opcodes;
pub mod parser;
pub mod style_parser;
pub use ast::{Directive, ModuleSoA, NodeId, Value};
pub use compiler::Compiler;
pub use parser::Parser;
pub use style_parser::StyleParser;
pub fn compile_project(gltm_src: &str, glts_src: &str) -> Result<Vec<u8>, String> {
let mut module = ModuleSoA::new();
let mut style_parser = StyleParser::new(glts_src, &mut module);
style_parser.parse_all()?;
let mut gltm_parser = Parser::with_module(gltm_src, module);
gltm_parser.parse_all()?;
let module = gltm_parser.module;
let compiler = Compiler::new(&module);
Ok(compiler.compile(&module.hierarchy))
}

View File

@@ -1,302 +0,0 @@
mod ast;
mod compiler;
mod opcodes;
mod parser;
use compiler::Compiler;
use parser::Parser;
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
const INPUT: &str = r#"
@version 1
@style "desktop.glts"
@global $username = !rhei: os.env("USER")
@global $hostname = !rhei: os.hostname()
@global $locale = "ru_RU"
@global $scaleFactor = 1.0
@singleton Config {
wallpaper = fs:/home/$username/.config/de/wallpaper.jpg
wallpaperFit = "cover"
iconTheme = "papirus-dark"
fontMain = "Inter"
fontMono = "JetBrains Mono"
fontSize = 13
workspaces = 4
animEnabled = true
accentColor = #cba6f7
taskbarPos = "bottom"
}
@singleton SystemState {
battery = !rhei: sys.battery()
volume = !rhei: sys.volume.get()
brightness = !rhei: sys.brightness.get()
network = !rhei: sys.network.status()
timezone = !rhei: os.timezone()
}
@component DesktopIcon(label: str, icon: fspath, exec: str) {
Button(class="desktop-icon") {
@on click {
!rhei: process.spawn($exec)
}
@on dblclick {
!rhei {
process.spawn($exec)
wm.raise(process.lastPid())
}
}
Image(src=$icon, class="icon-img") {}
Label(class="icon-label") $label
}
}
@component BatteryWidget {
@if $SystemState.battery.present {
@let $lvl = $SystemState.battery.level
@let $ico = !rhei: icons.battery($lvl)
Panel(class="tray-item") {
Icon(src=$ico, class="tray-icon") {}
@if $SystemState.battery.charging {
Icon(src=fs:/usr/share/glint-de/icons/charging.svg,
class="tray-icon tray-icon--accent") {}
}
}
}
}
@component ClockWidget {
@let $time = !rhei: sys.time.format("%H:%M", $SystemState.timezone)
@let $date = !rhei: sys.time.format("%d %b", $SystemState.timezone)
Panel(class="clock-widget") {
@on click {
!rhei: ui.toggle("calendar-popup")
}
Label(class="clock-time") $time
Label(class="clock-date") $date
}
}
@component SystemTray {
Panel(id="system-tray", class="tray") {
BatteryWidget {}
@if $SystemState.network.connected {
Icon(class="tray-icon",
src=!rhei: icons.network($SystemState.network.type)) {}
} @else {
Icon(class="tray-icon tray-icon--warn",
src=fs:/usr/share/glint-de/icons/net-offline.svg) {}
}
Slider(id="vol-slider", class="tray-slider",
value=$SystemState.volume, min=0, max=100) {
@on change {
!rhei: sys.volume.set($self.value)
}
}
ClockWidget {}
}
}
Screen(id="root", width=1920, height=1080, scale=$scaleFactor) {
Wallpaper(id="bg",
src=$Config.wallpaper,
fallback=fs:/usr/share/glint-de/wallpapers/default.jpg,
fit=$Config.wallpaperFit,
watch=true) {}
Panel(id="desktop", class="desktop") {
@let $desktopFiles = !rhei: fs.glob("/home/$username/Desktop/*.desktop")
@each $f in $desktopFiles {
@let $entry = !rhei: xdg.parseDesktop($f)
DesktopIcon(
label=$entry.name,
icon=!rhei: icons.resolve($entry.icon, $Config.iconTheme),
exec=$entry.exec
) {}
}
@on contextmenu {
!rhei: ui.show("desktop-ctx")
}
}
ContextMenu(id="desktop-ctx", class="ctx-menu", visible=false) {
MenuItem(class="ctx-item") {
@on click { !rhei: ui.show("settings") }
"Настройки рабочего стола"
}
MenuItem(class="ctx-item") {
@on click { !rhei: wallpaper.showPicker() }
"Сменить обои"
}
MenuDivider {}
MenuItem(class="ctx-item ctx-item--danger") {
@on click { !rhei: session.logout() }
"Выйти из сеанса"
}
}
Panel(id="taskbar", class="taskbar") {
Button(id="launcher-btn", class="launcher-button") {
@on click { !rhei: ui.toggle("app-launcher") }
Image(src=fs:/usr/share/glint-de/logo.svg) {}
}
Panel(id="window-list", class="window-list") {
@let $wins = !rhei: wm.getWindows()
@each $w in $wins {
Button(class="taskbar-win", data-wid=$w.id,
active=$w.focused) {
@on click { !rhei: wm.focus($w.id) }
@on middleclick { !rhei: wm.close($w.id) }
Icon(src=$w.icon, class="win-icon") {}
Label(class="win-title") $w.title
}
}
}
SystemTray {}
}
Overlay(id="app-launcher", class="launcher", visible=false) {
@on keydown(key="Escape") {
!rhei: ui.hide("app-launcher")
}
Input(id="launcher-search", class="launcher-search",
placeholder="Поиск приложений...",
autofocus=true) {
@on input {
!rhei: launcher.filter($self.value)
}
}
Grid(id="app-grid", class="launcher-grid", columns=6) {
@let $apps = !rhei: apps.listAll()
@each $app in $apps {
Button(class="launcher-app") {
@on click {
!rhei {
process.spawn($app.exec)
ui.hide("app-launcher")
}
}
Image(src=$app.icon, class="app-icon") {}
Label(class="app-name") $app.name
}
}
}
}
Overlay(id="notif-layer", class="notif-layer") {
@let $notifs = !rhei: notifications.active()
@each $n in $notifs {
Panel(class="notif-card", data-id=$n.id) {
Panel(class="notif-header") {
Icon(src=$n.appIcon, class="notif-icon") {}
Label(class="notif-app") $n.appName
Button(class="notif-close") {
@on click { !rhei: notifications.dismiss($n.id) }
"×"
}
}
Label(class="notif-title") $n.summary
@if $n.body {
Label(class="notif-body") $n.body
}
}
}
}
@if !rhei: os.env("GLINT_DEV") {
!rhei {
debug.overlay.show()
debug.log("DE инициализирован. Пользователь: " + $username)
debug.log("Синглтоны: Config, SystemState")
}
}
}
"#;
fn main() {
let mut parser = Parser::new(INPUT);
match parser.parse_all() {
Ok(_) => {
println!("Parse is successful");
let m = &parser.module;
let mut child_indices = HashSet::new();
for (start, len) in &m.elem_child_spans {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
for dir in &m.directives {
let span = match dir {
ast::Directive::Component { child_span, .. } => Some(child_span),
ast::Directive::If {
child_span,
else_span,
..
} => {
if let Some(es) = else_span {
for i in es.0..(es.0 + es.1) {
child_indices.insert(i as usize);
}
}
Some(child_span)
}
ast::Directive::Each { child_span, .. } => Some(child_span),
ast::Directive::On { child_span, .. } => Some(child_span),
_ => None,
};
if let Some((start, len)) = span {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
}
let mut root_nodes = Vec::new();
for (i, node) in m.hierarchy.iter().enumerate() {
if !child_indices.contains(&i) {
root_nodes.push(node.clone());
}
}
println!("Compile...");
let compiler = Compiler::new(m);
let bytecode = compiler.compile(&root_nodes);
let output_path = "desktop.glbc";
let mut file = File::create(output_path).expect("Не удалось создать файл");
file.write_all(&bytecode)
.expect("Не удалось записать байткод");
println!(
"Compile is successful. Compiled in {} ({} bytes)",
output_path,
bytecode.len()
);
}
Err(e) => {
eprintln!("❌ Error of parse: {}", e);
}
}
}
#[cfg(test)]
mod tests;

View File

@@ -11,6 +11,8 @@ pub const OP_IF: u8 = 0x07;
pub const OP_EACH: u8 = 0x08; pub const OP_EACH: u8 = 0x08;
pub const OP_ON: u8 = 0x09; pub const OP_ON: u8 = 0x09;
pub const OP_RHEI_BLK: u8 = 0x0A; pub const OP_RHEI_BLK: u8 = 0x0A;
pub const OP_PROP_NULL: u8 = 0x28;
pub const OP_PROP_ARRAY: u8 = 0x29;
// elements managment // elements managment
pub const OP_ELEM_PUSH: u8 = 0x10; pub const OP_ELEM_PUSH: u8 = 0x10;
@@ -27,4 +29,14 @@ pub const OP_PROP_FSPATH: u8 = 0x25;
pub const OP_PROP_VAR: u8 = 0x26; pub const OP_PROP_VAR: u8 = 0x26;
pub const OP_PROP_RHEI: u8 = 0x27; pub const OP_PROP_RHEI: u8 = 0x27;
pub const OP_STYLE_RULE: u8 = 0x0B;
pub const OP_STYLE_ANIM: u8 = 0x0C;
pub const OP_PROP_CALL: u8 = 0x2A;
pub const OP_PROP_UNIT: u8 = 0x2B;
pub const OP_PROP_IDENT: u8 = 0x2C;
pub const OP_END_BLOCK: u8 = 0xFF; pub const OP_END_BLOCK: u8 = 0xFF;

View File

@@ -7,20 +7,25 @@ pub struct Parser<'a> {
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
pub fn new(src: &'a str) -> Self { pub fn new(src: &'a str) -> Self {
Self::with_module(src, ModuleSoA::new())
}
pub fn with_module(src: &'a str, module: ModuleSoA) -> Self {
Self { Self {
src: src.as_bytes(), src: src.as_bytes(),
pos: 0, pos: 0,
module: ModuleSoA::new(), module,
} }
} }
#[inline] #[inline(always)]
fn peek(&self) -> Option<u8> { fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied() self.src.get(self.pos).copied()
} }
#[inline] #[inline(always)]
fn advance(&mut self) { fn advance(&mut self) {
self.pos += 1; self.pos += 1;
} }
@@ -36,15 +41,15 @@ impl<'a> Parser<'a> {
} }
fn skip_whitespace(&mut self) { fn skip_whitespace(&mut self) {
while let Some(c) = self.peek() { let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if c.is_ascii_whitespace() { if c.is_ascii_whitespace() {
self.advance(); self.pos += 1;
} else if c == b'/' && self.src.get(self.pos + 1) == Some(&b'/') { } else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' {
while let Some(cc) = self.peek() { self.pos += 2;
if cc == b'\n' { while self.pos < len && self.src[self.pos] != b'\n' {
break; self.pos += 1;
}
self.advance();
} }
} else { } else {
break; break;
@@ -54,16 +59,16 @@ impl<'a> Parser<'a> {
fn parse_ident(&mut self) -> String { fn parse_ident(&mut self) -> String {
let start = self.pos; let start = self.pos;
while let Some(c) = self.peek() { let len = self.src.len();
if c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c == b'.' { while self.pos < len {
self.advance(); let c = self.src[self.pos];
if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.') {
self.pos += 1;
} else { } else {
break; break;
} }
} }
std::str::from_utf8(&self.src[start..self.pos]) unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() }
.unwrap()
.to_string()
} }
fn parse_rhei_expr(&mut self) -> Result<String, String> { fn parse_rhei_expr(&mut self) -> Result<String, String> {
@@ -71,88 +76,92 @@ impl<'a> Parser<'a> {
if self.consume_if(b'{') { if self.consume_if(b'{') {
let start = self.pos; let start = self.pos;
let mut depth = 1; let mut depth = 1;
while depth > 0 { let len = self.src.len();
let c = self.peek().ok_or("Unexpected EOF in Rhei block")?;
self.advance(); while depth > 0 && self.pos < len {
if c == b'{' { match self.src[self.pos] {
depth += 1; b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
} }
if c == b'}' { self.pos += 1;
depth -= 1;
} }
if depth > 0 {
return Err("Unexpected EOF in Rhei block".into());
} }
let expr = std::str::from_utf8(&self.src[start..self.pos - 1])
.unwrap() let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos - 1]) };
.trim() Ok(expr.trim().to_string())
.to_string();
Ok(expr)
} else { } else {
let start = self.pos; let start = self.pos;
let mut parens = 0; let mut parens = 0;
let mut brackets = 0;
let mut in_str = false; let mut in_str = false;
let mut escape = false; let mut escape = false;
while let Some(c) = self.peek() { let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if escape { if escape {
escape = false; escape = false;
self.advance(); self.pos += 1;
continue; continue;
} }
if c == b'\\' {
escape = true; match c {
self.advance(); b'\\' => escape = true,
continue; b'"' => in_str = !in_str,
} b'(' if !in_str => parens += 1,
if c == b'"' { b')' if !in_str => {
in_str = !in_str; if parens == 0 { break; }
}
if !in_str {
if c == b'(' {
parens += 1;
}
if c == b')' {
if parens == 0 {
break;
}
parens -= 1; parens -= 1;
} }
if parens == 0 && (c == b',' || c == b'\n' || c == b'}' || c == b'{') { b'[' if !in_str => brackets += 1,
break; b']' if !in_str && brackets > 0 => brackets -= 1,
b',' | b'\n' | b'}' | b'{' if !in_str && parens == 0 && brackets == 0 => break,
_ => {}
} }
self.pos += 1;
} }
self.advance(); let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) };
} Ok(expr.trim().to_string())
let expr = std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.trim()
.to_string();
Ok(expr)
} }
} }
fn parse_value(&mut self) -> Result<Value, String> { fn parse_value(&mut self) -> Result<Value, String> {
self.skip_whitespace(); self.skip_whitespace();
let c = self.peek().ok_or("Expected value, found EOF")?; let c = self.peek().ok_or("Expected value, found EOF")?;
match c { match c {
b'"' => { b'"' => {
self.advance(); self.advance();
let start = self.pos; let start = self.pos;
while self.peek() != Some(b'"') { let len = self.src.len();
if self.peek().is_none() { while self.pos < len && self.src[self.pos] != b'"' {
self.pos += 1;
}
if self.pos >= len {
return Err("Unexpected EOF inside string literal".into()); return Err("Unexpected EOF inside string literal".into());
} }
self.advance(); let val = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string();
}
let val = std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.to_string();
self.advance(); self.advance();
Ok(Value::String(val)) Ok(Value::String(val))
} }
b'[' => {
self.advance();
let mut items = Vec::new();
loop {
self.skip_whitespace();
if self.consume_if(b']') { break; }
items.push(self.parse_value()?);
self.skip_whitespace();
self.consume_if(b',');
}
Ok(Value::Array(items))
}
b'#' => { b'#' => {
self.advance(); self.advance();
let mut color = String::from("#"); Ok(Value::Color(format!("#{}", self.parse_ident())))
color.push_str(&self.parse_ident());
Ok(Value::Color(color))
} }
b'$' => { b'$' => {
self.advance(); self.advance();
@@ -160,19 +169,16 @@ impl<'a> Parser<'a> {
} }
b'!' => { b'!' => {
self.advance(); self.advance();
let ident = self.parse_ident(); if self.parse_ident() != "rhei" { return Err("Expected !rhei".into()); }
if ident != "rhei" {
return Err("Expected !rhei".into());
}
self.consume_if(b':'); self.consume_if(b':');
Ok(Value::Rhei(self.parse_rhei_expr()?)) Ok(Value::Rhei(self.parse_rhei_expr()?))
} }
b'0'..=b'9' | b'-' => { b'0'..=b'9' | b'-' => {
let s = self.parse_ident(); let s = self.parse_ident();
if s.contains('.') { if s.contains('.') {
Ok(Value::Float(s.parse().unwrap())) s.parse().map(Value::Float).map_err(|_| format!("Invalid float: {}", s))
} else { } else {
Ok(Value::Int(s.parse().unwrap())) s.parse().map(Value::Int).map_err(|_| format!("Invalid int: {}", s))
} }
} }
_ => { _ => {
@@ -180,18 +186,19 @@ impl<'a> Parser<'a> {
match s.as_str() { match s.as_str() {
"true" => Ok(Value::Bool(true)), "true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)), "false" => Ok(Value::Bool(false)),
"null" => Ok(Value::Null),
"fs" => { "fs" => {
self.consume_if(b':'); self.consume_if(b':');
let start = self.pos; let start = self.pos;
while let Some(cc) = self.peek() { let len = self.src.len();
if cc.is_ascii_whitespace() || cc == b',' || cc == b')' || cc == b'}' { while self.pos < len {
let cc = self.src[self.pos];
if cc.is_ascii_whitespace() || matches!(cc, b',' | b')' | b'}') {
break; break;
} }
self.advance(); self.pos += 1;
} }
let path = std::str::from_utf8(&self.src[start..self.pos]) let path = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.to_string();
.unwrap()
.to_string();
Ok(Value::FsPath(path)) Ok(Value::FsPath(path))
} }
_ => Err(format!("Unknown value token: {}", s)), _ => Err(format!("Unknown value token: {}", s)),
@@ -205,22 +212,22 @@ impl<'a> Parser<'a> {
self.advance(); self.advance();
loop { loop {
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b')') { if self.consume_if(b')') { break; }
break;
}
let key = self.parse_ident(); let key = self.parse_ident();
if key.is_empty() { if key.is_empty() {
return Err(format!( return Err(format!("Expected property identifier, found {:?}", self.peek().map(|b| b as char)));
"Expected property identifier, found {:?}",
self.peek().map(|b| b as char)
));
} }
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b'=') { if !self.consume_if(b'=') {
return Err(format!("Expected '=' after property key '{}', found {:?}", key, self.peek().map(|b| b as char)));
}
let val = self.parse_value()?; let val = self.parse_value()?;
self.module.prop_keys.push(key); self.module.prop_keys.push(key);
self.module.prop_values.push(val); self.module.prop_values.push(val);
}
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b','); self.consume_if(b',');
} }
@@ -237,11 +244,8 @@ impl<'a> Parser<'a> {
let mut direct_children = Vec::new(); let mut direct_children = Vec::new();
loop { loop {
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b'}') { if self.consume_if(b'}') { break; }
break; direct_children.push(self.parse_node()?);
}
let node = self.parse_node()?;
direct_children.push(node);
} }
let start_idx = self.module.hierarchy.len() as u32; let start_idx = self.module.hierarchy.len() as u32;
@@ -256,135 +260,102 @@ impl<'a> Parser<'a> {
let dir = match name.as_str() { let dir = match name.as_str() {
"version" => { "version" => {
self.skip_whitespace(); self.skip_whitespace();
let v = self.parse_ident().parse().unwrap(); Directive::Version(self.parse_ident().parse().map_err(|_| "Invalid version")?)
Directive::Version(v)
} }
"style" => Directive::Style(match self.parse_value()? { "style" => match self.parse_value()? {
Value::String(s) => s, Value::String(s) => Directive::Style(s),
_ => return Err("Style must be a string".into()), _ => return Err("Style must be a string".into()),
}), },
"global" => { "global" => {
self.skip_whitespace(); self.skip_whitespace();
if !self.consume_if(b'$') { if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @global".into()); }
return Err("Expected $ var".into()); let name = self.parse_ident();
}
let var_name = self.parse_ident();
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'='); if !self.consume_if(b'=') { return Err(format!("Expected '=' after global variable name '${}'", name)); }
let val = self.parse_value()?; Directive::Global { name, value: self.parse_value()? }
Directive::Global {
name: var_name,
value: val,
}
} }
"singleton" => { "singleton" => {
self.skip_whitespace(); self.skip_whitespace();
let sname = self.parse_ident(); let name = self.parse_ident();
self.skip_whitespace(); self.skip_whitespace();
let start_idx = self.module.prop_keys.len() as u32; let start_idx = self.module.prop_keys.len() as u32;
if self.consume_if(b'{') { if !self.consume_if(b'{') { return Err(format!("Expected '{{' for singleton '{}'", name)); }
loop { loop {
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b'}') { if self.consume_if(b'}') { break; }
break;
}
let key = self.parse_ident(); let key = self.parse_ident();
if key.is_empty() { if key.is_empty() { return Err("Expected singleton property identifier".into()); }
return Err(format!(
"Expected singleton property identifier, found {:?}",
self.peek().map(|b| b as char)
));
}
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'='); if !self.consume_if(b'=') { return Err(format!("Expected '=' after singleton property key '{}'", key)); }
let val = self.parse_value()?; let val = self.parse_value()?;
self.module.prop_keys.push(key); self.module.prop_keys.push(key);
self.module.prop_values.push(val); self.module.prop_values.push(val);
}
self.skip_whitespace();
self.consume_if(b',');
} }
let len = (self.module.prop_keys.len() as u32) - start_idx; let len = (self.module.prop_keys.len() as u32) - start_idx;
Directive::Singleton { Directive::Singleton { name, prop_span: (start_idx, len) }
name: sname,
prop_span: (start_idx, len),
}
} }
"component" => { "component" => {
self.skip_whitespace(); self.skip_whitespace();
let cname = self.parse_ident(); let name = self.parse_ident();
let mut params = Vec::new(); let mut params = Vec::new();
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b'(') { if self.consume_if(b'(') {
loop { loop {
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b')') { if self.consume_if(b')') { break; }
break;
}
let pname = self.parse_ident(); let pname = self.parse_ident();
if pname.is_empty() { return Err("Expected parameter name in @component".into()); }
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b':'); if !self.consume_if(b':') { return Err(format!("Expected ':' after parameter '{}'", pname)); }
self.skip_whitespace(); self.skip_whitespace();
let ptype = self.parse_ident(); let ptype = self.parse_ident();
if ptype.is_empty() { return Err(format!("Expected type after ':' for parameter '{}'", pname)); }
params.push((pname, ptype)); params.push((pname, ptype));
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b','); self.consume_if(b',');
} }
} }
let child_span = self.parse_block()?; Directive::Component { name, params, child_span: self.parse_block()? }
Directive::Component {
name: cname,
params,
child_span,
}
} }
"let" => { "let" => {
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'$'); if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @let".into()); }
let var_name = self.parse_ident(); let name = self.parse_ident();
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'='); if !self.consume_if(b'=') { return Err(format!("Expected '=' after variable name '${}'", name)); }
let val = self.parse_value()?; Directive::Let { name, value: self.parse_value()? }
Directive::Let {
name: var_name,
value: val,
}
} }
"if" => { "if" => {
self.skip_whitespace();
let condition = self.parse_value()?; let condition = self.parse_value()?;
let child_span = self.parse_block()?; let child_span = self.parse_block()?;
let mut else_span = None; let mut else_span = None;
self.skip_whitespace();
self.skip_whitespace();
let backup = self.pos; let backup = self.pos;
if self.consume_if(b'@') { if self.consume_if(b'@') {
if self.parse_ident() == "else" { if self.parse_ident() == "else" {
else_span = Some(self.parse_block()?); else_span = Some(self.parse_block()?);
} else { } else {
self.pos = backup; self.pos = backup; // rollback
} }
} }
Directive::If { Directive::If { condition, child_span, else_span }
condition,
child_span,
else_span,
}
} }
"each" => { "each" => {
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'$'); if !self.consume_if(b'$') { return Err("Expected '$' before item identifier in @each".into()); }
let item = self.parse_ident(); let item = self.parse_ident();
self.skip_whitespace(); self.skip_whitespace();
let in_kw = self.parse_ident(); if self.parse_ident() != "in" { return Err("Expected 'in' keyword in @each".into()); }
if in_kw != "in" { Directive::Each { item, collection: self.parse_value()?, child_span: self.parse_block()? }
return Err("Expected 'in' in @each".into());
}
let collection = self.parse_value()?;
let child_span = self.parse_block()?;
Directive::Each {
item,
collection,
child_span,
}
} }
"on" => { "on" => {
self.skip_whitespace(); self.skip_whitespace();
@@ -394,24 +365,17 @@ impl<'a> Parser<'a> {
if self.consume_if(b'(') { if self.consume_if(b'(') {
loop { loop {
self.skip_whitespace(); self.skip_whitespace();
if self.consume_if(b')') { if self.consume_if(b')') { break; }
break;
}
let k = self.parse_ident(); let k = self.parse_ident();
if k.is_empty() { return Err("Expected argument name in @on".into()); }
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b'='); if !self.consume_if(b'=') { return Err(format!("Expected '=' after event argument key '{}'", k)); }
let v = self.parse_value()?; args.push((k, self.parse_value()?));
args.push((k, v));
self.skip_whitespace(); self.skip_whitespace();
self.consume_if(b','); self.consume_if(b',');
} }
} }
let child_span = self.parse_block()?; Directive::On { event, args, child_span: self.parse_block()? }
Directive::On {
event,
args,
child_span,
}
} }
_ => return Err(format!("Unknown directive: @{}", name)), _ => return Err(format!("Unknown directive: @{}", name)),
}; };
@@ -424,22 +388,31 @@ impl<'a> Parser<'a> {
self.skip_whitespace(); self.skip_whitespace();
if self.peek() == Some(b'(') { if self.peek() == Some(b'(') {
let span = self.parse_properties()?; self.module.elem_prop_spans[el_id as usize] = self.parse_properties()?;
self.module.elem_prop_spans[el_id as usize] = span;
} }
self.skip_whitespace(); self.skip_whitespace();
let p = self.peek(); match self.peek() {
if p == Some(b'{') { Some(b'{') => {
let span = self.parse_block()?; self.module.elem_child_spans[el_id as usize] = self.parse_block()?;
self.module.elem_child_spans[el_id as usize] = span; }
} else if p == Some(b'"') || p == Some(b'$') { Some(ch) if matches!(ch, b'"' | b'#' | b'$' | b'!' | b'-' | b'[' | b'0'..=b'9') => {
let val = self.parse_value()?; self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
self.module.elem_content[el_id as usize] = Some(val); }
Some(b'a'..=b'z') => {
let backup = self.pos;
let ident = self.parse_ident();
self.pos = backup;
if matches!(ident.as_str(), "true" | "false" | "null" | "fs") {
self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
}
}
_ => {}
} }
Ok(NodeId::Element(el_id)) Ok(NodeId::Element(el_id))
} }
pub fn parse_node(&mut self) -> Result<NodeId, String> { pub fn parse_node(&mut self) -> Result<NodeId, String> {
self.skip_whitespace(); self.skip_whitespace();
@@ -449,24 +422,24 @@ impl<'a> Parser<'a> {
b'@' => self.parse_directive(), b'@' => self.parse_directive(),
b'!' => { b'!' => {
self.advance(); self.advance();
let i = self.parse_ident(); if self.parse_ident() != "rhei" { return Err("Expected rhei".into()); }
if i != "rhei" {
return Err("Expected rhei".into());
}
self.consume_if(b':'); self.consume_if(b':');
let expr = self.parse_rhei_expr()?; let expr = self.parse_rhei_expr()?;
Ok(NodeId::Directive( Ok(NodeId::Directive(self.module.push_directive(Directive::RheiBlock(expr))))
self.module.push_directive(Directive::RheiBlock(expr)),
))
} }
b'A'..=b'Z' => self.parse_element(), b'A'..=b'Z' => self.parse_element(),
b'"' | b'$' => { ch if matches!(ch, b'"' | b'#' | b'$' | b'-' | b'[' | b'0'..=b'9') => {
let val = self.parse_value()?; let val = self.parse_value()?;
let el_id = self.module.push_element("#text".to_string()); let el_id = self.module.push_element("#text".to_string());
self.module.elem_content[el_id as usize] = Some(val); self.module.elem_content[el_id as usize] = Some(val);
Ok(NodeId::Element(el_id)) Ok(NodeId::Element(el_id))
} }
_ => Err(format!("Unexpected token: {}", c as char)), _ => {
let extract_start = self.pos.saturating_sub(10);
let extract_end = std::cmp::min(self.src.len(), self.pos + 20);
let context = std::str::from_utf8(&self.src[extract_start..extract_end]).unwrap_or("");
Err(format!("Unexpected token: '{}' at position {}. Context: \"...{}...\"", c as char, self.pos, context.trim()))
}
} }
} }

312
src/style_parser.rs Normal file
View File

@@ -0,0 +1,312 @@
use crate::ast::*;
use std::collections::HashMap;
pub struct StyleParser<'a> {
src: &'a [u8],
pos: usize,
pub module: &'a mut ModuleSoA,
variables: HashMap<String, Value>,
mixins: HashMap<String, Vec<(String, Value)>>,
}
impl<'a> StyleParser<'a> {
pub fn new(src: &'a str, module: &'a mut ModuleSoA) -> Self {
Self {
src: src.as_bytes(),
pos: 0,
module,
variables: HashMap::new(),
mixins: HashMap::new(),
}
}
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
fn advance(&mut self) {
self.pos += 1;
}
fn consume_if(&mut self, expected: u8) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
fn skip_whitespace(&mut self) {
let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if c.is_ascii_whitespace() {
self.pos += 1;
} else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' {
self.pos += 2;
while self.pos < len && self.src[self.pos] != b'\n' {
self.pos += 1;
}
} else {
break;
}
}
}
fn parse_ident(&mut self) -> String {
let start = self.pos;
while self.pos < self.src.len() {
let c = self.src[self.pos];
if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_') {
self.pos += 1;
} else {
break;
}
}
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() }
}
fn parse_selector(&mut self) -> String {
let start = self.pos;
while self.pos < self.src.len() && self.src[self.pos] != b'{' {
self.pos += 1;
}
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).trim().to_string() }
}
fn parse_single_value(&self, s: &str) -> Value {
let s = s.trim_end_matches(',');
if let Some(stripped) = s.strip_prefix('#') {
return Value::Color(format!("#{}", stripped));
}
if let Some(var_name) = s.strip_prefix('$') {
if let Some(val) = self.variables.get(var_name) {
return val.clone();
}
return Value::Variable(var_name.to_string());
}
if s.contains('(') && s.ends_with(')') {
let parts: Vec<&str> = s.splitn(2, '(').collect();
let name = parts[0].trim().to_string();
let args_str = parts[1].trim_end_matches(')');
let args = args_str.split(',')
.filter(|a| !a.trim().is_empty())
.map(|a| self.parse_single_value(a.trim()))
.collect();
return Value::Call(name, args);
}
let mut num_end = 0;
for (i, c) in s.char_indices() {
if c.is_ascii_digit() || c == '.' || c == '-' {
num_end = i + c.len_utf8();
} else { break; }
}
if num_end > 0 && num_end < s.len() {
if let Ok(num) = s[..num_end].parse::<f64>() {
return Value::Unit(num, s[num_end..].to_string());
}
}
if let Ok(i) = s.parse::<i64>() { return Value::Int(i); }
if let Ok(f) = s.parse::<f64>() { return Value::Float(f); }
if s == "true" { return Value::Bool(true); }
if s == "false" { return Value::Bool(false); }
if s == "null" { return Value::Null; }
Value::Ident(s.to_string())
}
fn parse_value_line(&mut self) -> Value {
let start = self.pos;
let mut parens = 0;
while self.pos < self.src.len() {
let c = self.src[self.pos];
if c == b'(' { parens += 1; }
else if c == b')' { parens -= 1; }
else if parens == 0 && matches!(c, b'\n' | b'\r' | b';' | b'}') {
break;
}
self.pos += 1;
}
let line = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim();
if self.peek() == Some(b';') { self.advance(); }
let mut tokens = Vec::new();
let mut current = String::new();
let mut p = 0;
for c in line.chars() {
match c {
'(' => { p += 1; current.push(c); }
')' => { p -= 1; current.push(c); }
' ' | '\t' if p == 0 => {
if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
}
_ => current.push(c),
}
}
if !current.is_empty() { tokens.push(current); }
if tokens.len() == 1 {
self.parse_single_value(&tokens[0])
} else {
Value::Array(tokens.into_iter().map(|t| self.parse_single_value(&t)).collect())
}
}
fn parse_properties(&mut self, target: &mut Vec<(String, Value)>) -> Result<(), String> {
self.consume_if(b'{');
loop {
self.skip_whitespace();
if self.consume_if(b'}') { break; }
if self.peek() == Some(b'@') {
self.advance();
let ident = self.parse_ident();
if ident == "use" {
self.skip_whitespace();
let mixin_name = self.parse_ident();
if let Some(props) = self.mixins.get(&mixin_name) {
target.extend(props.clone());
} else {
return Err(format!("Unknown mixin: {}", mixin_name));
}
}
continue;
}
let key = self.parse_ident();
if key.is_empty() { return Err("Expected property key".into()); }
self.skip_whitespace();
if !self.consume_if(b':') { return Err("Expected ':' after property".into()); }
self.skip_whitespace();
let val = self.parse_value_line();
target.push((key, val));
}
Ok(())
}
fn parse_top_level(&mut self) -> Result<Option<NodeId>, String> {
match self.peek() {
Some(b'$') => {
self.advance();
let name = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'=');
self.skip_whitespace();
let val = self.parse_value_line();
self.variables.insert(name, val);
Ok(None)
}
Some(b'@') => {
self.advance();
let name = self.parse_ident();
match name.as_str() {
"mixin" => {
self.skip_whitespace();
let mixin_name = self.parse_ident();
self.skip_whitespace();
let mut props = Vec::new();
self.parse_properties(&mut props)?;
self.mixins.insert(mixin_name, props);
Ok(None)
}
"anim" => {
self.skip_whitespace();
let anim_name = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'{');
let mut frames = Vec::new();
loop {
self.skip_whitespace();
if self.consume_if(b'}') { break; }
let step = self.parse_selector();
let mut props = Vec::new();
self.parse_properties(&mut props)?;
let start_idx = self.module.prop_keys.len() as u32;
for (k, v) in props {
self.module.prop_keys.push(k);
self.module.prop_values.push(v);
}
let len = self.module.prop_keys.len() as u32 - start_idx;
frames.push((step, (start_idx, len)));
}
let id = self.module.push_directive(Directive::StyleAnim { name: anim_name, frames });
Ok(Some(NodeId::Directive(id)))
}
"if" => {
self.skip_whitespace();
let start = self.pos;
while self.pos < self.src.len() && self.src[self.pos] != b'{' { self.pos += 1; }
let cond_str = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim();
self.consume_if(b'{');
let start_idx = self.module.hierarchy.len() as u32;
loop {
self.skip_whitespace();
if self.consume_if(b'}') { break; }
if let Some(node) = self.parse_top_level()? {
self.module.hierarchy.push(node);
}
}
let len = self.module.hierarchy.len() as u32 - start_idx;
// Condition evaluates at runtime via Rhei
let condition = Value::Rhei(cond_str.to_string());
let id = self.module.push_directive(Directive::If {
condition,
child_span: (start_idx, len),
else_span: None
});
Ok(Some(NodeId::Directive(id)))
}
_ => Err(format!("Unknown directive: @{}", name)),
}
}
_ => {
let selector = self.parse_selector();
if selector.is_empty() { return Err("Expected selector".into()); }
let mut props = Vec::new();
self.parse_properties(&mut props)?;
let start_idx = self.module.prop_keys.len() as u32;
for (k, v) in props {
self.module.prop_keys.push(k);
self.module.prop_values.push(v);
}
let len = self.module.prop_keys.len() as u32 - start_idx;
let id = self.module.push_directive(Directive::StyleRule { selector, prop_span: (start_idx, len) });
Ok(Some(NodeId::Directive(id)))
}
}
}
pub fn parse_all(&mut self) -> Result<(), String> {
self.skip_whitespace();
while self.pos < self.src.len() {
if let Some(node) = self.parse_top_level()? {
self.module.hierarchy.push(node);
}
self.skip_whitespace();
}
Ok(())
}
}

View File

@@ -257,3 +257,272 @@ fn test_full_desktop_compilation() {
); );
assert!(bytecode.len() > 50, "Байткод подозрительно мал"); assert!(bytecode.len() > 50, "Байткод подозрительно мал");
} }
#[cfg(test)]
mod style_parser_tests {
use crate::ast::{Directive, ModuleSoA, NodeId, Value};
use crate::style_parser::StyleParser;
#[test]
fn test_style_parser_basic_rule() {
let input = "Screen { background: transparent; color: #fff }";
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().expect("Парсинг базового правила должен пройти успешно");
assert_eq!(m.hierarchy.len(), 1, "Должна быть одна директива");
assert_eq!(m.directives.len(), 1);
if let Directive::StyleRule { selector, prop_span } = &m.directives[0] {
assert_eq!(selector, "Screen");
assert_eq!(prop_span.1, 2, "Должно быть 2 свойства");
let start = prop_span.0 as usize;
assert_eq!(m.prop_keys[start], "background");
assert_eq!(m.prop_values[start], Value::Ident("transparent".to_string()));
assert_eq!(m.prop_keys[start + 1], "color");
assert_eq!(m.prop_values[start + 1], Value::Color("#fff".to_string()));
} else {
panic!("Ожидался узел StyleRule");
}
}
#[test]
fn test_style_parser_compile_time_variables() {
let input = r#"
$bg = #1e1e2e
$pad = 16px
Panel {
background: $bg
padding: $pad
color: $Config.textColor
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().expect("Парсинг с переменными");
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
let start = prop_span.0 as usize;
assert_eq!(m.prop_values[start], Value::Color("#1e1e2e".to_string()));
assert_eq!(m.prop_values[start + 1], Value::Unit(16.0, "px".to_string()));
assert_eq!(m.prop_values[start + 2], Value::Variable("Config.textColor".to_string()));
} else {
panic!("Ожидался StyleRule");
}
}
#[test]
fn test_style_parser_mixins() {
let input = r#"
@mixin flex-center {
display: flex
align-items: center
}
Box {
@use flex-center
width: 100%
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
assert_eq!(prop_span.1, 3, "Свойства из миксина должны добавиться к правилу");
let start = prop_span.0 as usize;
assert_eq!(m.prop_keys[start], "display");
assert_eq!(m.prop_values[start], Value::Ident("flex".to_string()));
assert_eq!(m.prop_keys[start + 1], "align-items");
assert_eq!(m.prop_values[start + 1], Value::Ident("center".to_string()));
assert_eq!(m.prop_keys[start + 2], "width");
assert_eq!(m.prop_values[start + 2], Value::Unit(100.0, "%".to_string()));
} else {
panic!("Ожидался StyleRule");
}
}
#[test]
fn test_style_parser_values_and_calls() {
let input = r#"
Overlay {
background: rgba(30, 30, 46, 0.88)
inset: 0 0 36px 0
box-shadow: 0 2px 8px rgba(0,0,0,0.45)
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
let start = match &m.directives[0] {
Directive::StyleRule { prop_span, .. } => prop_span.0 as usize,
_ => panic!("Ожидался StyleRule"),
};
// background: rgba(...)
assert_eq!(
m.prop_values[start],
Value::Call(
"rgba".to_string(),
vec![Value::Int(30), Value::Int(30), Value::Int(46), Value::Float(0.88)]
)
);
// inset: 0 0 36px 0 (парсится как Array)
assert_eq!(
m.prop_values[start + 1],
Value::Array(vec![
Value::Int(0),
Value::Int(0),
Value::Unit(36.0, "px".to_string()),
Value::Int(0)
])
);
if let Value::Array(arr) = &m.prop_values[start + 2] {
assert_eq!(arr[0], Value::Int(0));
assert_eq!(arr[1], Value::Unit(2.0, "px".to_string()));
assert_eq!(arr[2], Value::Unit(8.0, "px".to_string()));
assert!(matches!(arr[3], Value::Call(..)));
} else {
panic!("box-shadow должен быть спарсен как массив (набор значений)");
}
}
#[test]
fn test_style_parser_animation() {
let input = r#"
@anim fade-in {
from { opacity: 0 }
to { opacity: 1 }
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
if let Directive::StyleAnim { name, frames } = &m.directives[0] {
assert_eq!(name, "fade-in");
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].0, "from");
assert_eq!(frames[1].0, "to");
} else {
panic!("Ожидалась директива StyleAnim");
}
}
}
#[cfg(test)]
mod style_compiler_tests {
use crate::ast::ModuleSoA;
use crate::compiler::Compiler;
use crate::opcodes::*;
use crate::style_parser::StyleParser;
struct BytecodeBuilder {
buf: Vec<u8>,
}
impl BytecodeBuilder {
fn new() -> Self {
let mut b = Self { buf: Vec::new() };
b.buf.extend_from_slice(&MAGIC_HEADER);
b
}
fn push_u8(mut self, val: u8) -> Self {
self.buf.push(val);
self
}
fn push_u32(mut self, val: u32) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn push_str(mut self, s: &str) -> Self {
let bytes = s.as_bytes();
self.buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
self.buf.extend_from_slice(bytes);
self
}
fn push_f64(mut self, val: f64) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn build(self) -> Vec<u8> {
self.buf
}
}
#[test]
fn test_compiler_style_exact_bytes() {
let input = "Box { padding: 8px; color: transparent }";
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
let compiler = Compiler::new(&m);
let bytecode = compiler.compile(&m.hierarchy);
let expected = BytecodeBuilder::new()
.push_u8(OP_STYLE_RULE)
.push_str("Box")
.push_u32(2)
// 1: padding: 8px
.push_str("padding")
.push_u8(OP_PROP_UNIT)
.push_f64(8.0)
.push_str("px")
// 2: color: transparent
.push_str("color")
.push_u8(OP_PROP_IDENT)
.push_str("transparent")
.build();
assert_eq!(bytecode, expected);
}
}
#[cfg(test)]
mod integration_tests {
use crate::compile_project;
use crate::opcodes::MAGIC_HEADER;
const TEST_GLTS: &str = r#"
$accent = #cba6f7
@mixin flex-col {
display: flex
flex-direction: column
}
Panel.desktop {
@use flex-col
background: rgba(30, 30, 46, 0.88)
border: 1px solid $accent
}
"#;
const TEST_GLTM: &str = r#"
@version 1
Panel(id="main", class="desktop") {
Button(label="Start")
}
"#;
#[test]
fn test_full_compile_project() {
let result = compile_project(TEST_GLTM, TEST_GLTS);
assert!(result.is_ok(), "Проект должен успешно скомпилироваться: {:?}", result.err());
let bytecode = result.unwrap();
assert_eq!(
&bytecode[0..4],
&MAGIC_HEADER,
"Байткод должен начинаться с магического заголовка GLBC"
);
assert!(bytecode.len() > 100, "Байткод слишком мал, возможно скомпилировалось не все");
}
}