Compare commits
3 Commits
746643527d
...
v0.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 018a799c05 | |||
| ec78286165 | |||
| f2688072ac |
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -225,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -392,6 +392,12 @@ dependencies = [
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
@@ -772,6 +778,7 @@ dependencies = [
|
||||
name = "lux"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"postgres",
|
||||
@@ -1182,7 +1189,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1475,7 +1482,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lux"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
edition = "2021"
|
||||
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
|
||||
license = "MIT"
|
||||
@@ -17,6 +17,7 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking",
|
||||
tiny_http = "0.12"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
postgres = "0.19"
|
||||
glob = "0.3"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
printf "\n"
|
||||
printf " \033[1;35m╦ ╦ ╦╦ ╦\033[0m\n"
|
||||
printf " \033[1;35m║ ║ ║╔╣\033[0m\n"
|
||||
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.6\n"
|
||||
printf " \033[1;35m╩═╝╚═╝╩ ╩\033[0m v0.1.7\n"
|
||||
printf "\n"
|
||||
printf " Functional language with first-class effects\n"
|
||||
printf "\n"
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.6";
|
||||
version = "0.1.7";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
};
|
||||
in muslPkgs.rustPlatform.buildRustPackage {
|
||||
pname = "lux";
|
||||
version = "0.1.6";
|
||||
version = "0.1.7";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
|
||||
@@ -1166,6 +1166,7 @@ impl CBackend {
|
||||
self.writeln(" void (*mkdir)(void* env, LuxString path);");
|
||||
self.writeln(" void (*copy)(void* env, LuxString src, LuxString dst);");
|
||||
self.writeln(" LuxList* (*readDir)(void* env, LuxString path);");
|
||||
self.writeln(" LuxList* (*glob)(void* env, LuxString pattern);");
|
||||
self.writeln(" void* env;");
|
||||
self.writeln("} LuxFileHandler;");
|
||||
self.writeln("");
|
||||
@@ -1386,6 +1387,23 @@ impl CBackend {
|
||||
self.writeln(" return result;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("#include <glob.h>");
|
||||
self.writeln("static LuxList* lux_file_glob(LuxString pattern) {");
|
||||
self.writeln(" LuxList* result = lux_list_new(16);");
|
||||
self.writeln(" glob_t globbuf;");
|
||||
self.writeln(" int ret = glob(pattern, 0, NULL, &globbuf);");
|
||||
self.writeln(" if (ret == 0) {");
|
||||
self.writeln(" for (size_t i = 0; i < globbuf.gl_pathc; i++) {");
|
||||
self.writeln(" size_t len = strlen(globbuf.gl_pathv[i]);");
|
||||
self.writeln(" LuxString name = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
|
||||
self.writeln(" memcpy(name, globbuf.gl_pathv[i], len + 1);");
|
||||
self.writeln(" lux_list_push(result, (void*)name);");
|
||||
self.writeln(" }");
|
||||
self.writeln(" globfree(&globbuf);");
|
||||
self.writeln(" }");
|
||||
self.writeln(" return result;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static LuxString default_file_read(void* env, LuxString path) {");
|
||||
self.writeln(" (void)env;");
|
||||
self.writeln(" return lux_file_read(path);");
|
||||
@@ -1431,6 +1449,11 @@ impl CBackend {
|
||||
self.writeln(" return lux_file_readDir(path);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static LuxList* default_file_glob(void* env, LuxString pattern) {");
|
||||
self.writeln(" (void)env;");
|
||||
self.writeln(" return lux_file_glob(pattern);");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("static LuxFileHandler default_file_handler = {");
|
||||
self.writeln(" .read = default_file_read,");
|
||||
self.writeln(" .write = default_file_write,");
|
||||
@@ -1441,6 +1464,7 @@ impl CBackend {
|
||||
self.writeln(" .mkdir = default_file_mkdir,");
|
||||
self.writeln(" .copy = default_file_copy,");
|
||||
self.writeln(" .readDir = default_file_readDir,");
|
||||
self.writeln(" .glob = default_file_glob,");
|
||||
self.writeln(" .env = NULL");
|
||||
self.writeln("};");
|
||||
self.writeln("");
|
||||
@@ -3790,6 +3814,17 @@ impl CBackend {
|
||||
self.register_rc_var(&temp, "LuxList*");
|
||||
return Ok(temp);
|
||||
}
|
||||
"glob" => {
|
||||
let pattern = self.emit_expr(&args[0])?;
|
||||
let temp = format!("_glob_{}", self.fresh_name());
|
||||
if self.has_evidence {
|
||||
self.writeln(&format!("LuxList* {} = ev->file->glob(ev->file->env, {});", temp, pattern));
|
||||
} else {
|
||||
self.writeln(&format!("LuxList* {} = lux_file_glob({});", temp, pattern));
|
||||
}
|
||||
self.register_rc_var(&temp, "LuxList*");
|
||||
return Ok(temp);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -5353,7 +5388,7 @@ impl CBackend {
|
||||
"read" => Some("LuxString".to_string()),
|
||||
"write" | "append" | "delete" | "mkdir" | "copy" => Some("void".to_string()),
|
||||
"exists" | "isDir" => Some("LuxBool".to_string()),
|
||||
"readDir" | "listDir" => Some("LuxList*".to_string()),
|
||||
"readDir" | "listDir" | "glob" => Some("LuxList*".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
} else if effect.name == "Http" {
|
||||
|
||||
@@ -3968,6 +3968,29 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
("File", "glob") => {
|
||||
let pattern = match request.args.first() {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
_ => return Err(RuntimeError {
|
||||
message: "File.glob requires a string pattern".to_string(),
|
||||
span: None,
|
||||
}),
|
||||
};
|
||||
match glob::glob(&pattern) {
|
||||
Ok(paths) => {
|
||||
let entries: Vec<Value> = paths
|
||||
.filter_map(|entry| entry.ok())
|
||||
.map(|path| Value::String(path.to_string_lossy().to_string()))
|
||||
.collect();
|
||||
Ok(Value::List(entries))
|
||||
}
|
||||
Err(e) => Err(RuntimeError {
|
||||
message: format!("Invalid glob pattern '{}': {}", pattern, e),
|
||||
span: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Process Effect =====
|
||||
("Process", "exec") => {
|
||||
use std::process::Command;
|
||||
|
||||
@@ -964,6 +964,11 @@ impl TypeEnv {
|
||||
],
|
||||
return_type: Type::Unit,
|
||||
},
|
||||
EffectOpDef {
|
||||
name: "glob".to_string(),
|
||||
params: vec![("pattern".to_string(), Type::String)],
|
||||
return_type: Type::List(Box::new(Type::String)),
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
pub type Html<M> =
|
||||
| Element(String, List<Attr<M>>, List<Html<M>>)
|
||||
| Text(String)
|
||||
| RawHtml(String)
|
||||
| Empty
|
||||
|
||||
// Attributes that can be applied to elements
|
||||
@@ -41,6 +42,7 @@ pub type Attr<M> =
|
||||
| OnKeyDown(fn(String): M)
|
||||
| OnKeyUp(fn(String): M)
|
||||
| DataAttr(String, String)
|
||||
| Attribute(String, String)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Container elements
|
||||
@@ -180,6 +182,28 @@ pub fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("audio", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Document / Head elements
|
||||
// ============================================================================
|
||||
|
||||
pub fn meta<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("meta", attrs, [])
|
||||
|
||||
pub fn link<M>(attrs: List<Attr<M>>): Html<M> =
|
||||
Element("link", attrs, [])
|
||||
|
||||
pub fn script<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("script", attrs, children)
|
||||
|
||||
pub fn iframe<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("iframe", attrs, children)
|
||||
|
||||
pub fn figure<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("figure", attrs, children)
|
||||
|
||||
pub fn figcaption<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
||||
Element("figcaption", attrs, children)
|
||||
|
||||
// ============================================================================
|
||||
// Element builders - Tables
|
||||
// ============================================================================
|
||||
@@ -285,6 +309,12 @@ pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
||||
pub fn data<M>(name: String, value: String): Attr<M> =
|
||||
DataAttr(name, value)
|
||||
|
||||
pub fn attr<M>(name: String, value: String): Attr<M> =
|
||||
Attribute(name, value)
|
||||
|
||||
pub fn rawHtml<M>(content: String): Html<M> =
|
||||
RawHtml(content)
|
||||
|
||||
// ============================================================================
|
||||
// Utility functions
|
||||
// ============================================================================
|
||||
@@ -319,6 +349,7 @@ pub fn renderAttr<M>(attr: Attr<M>): String =
|
||||
Checked(false) => "",
|
||||
Name(n) => " name=\"" + n + "\"",
|
||||
DataAttr(name, value) => " data-" + name + "=\"" + value + "\"",
|
||||
Attribute(name, value) => " " + name + "=\"" + value + "\"",
|
||||
// Event handlers are ignored in static rendering
|
||||
OnClick(_) => "",
|
||||
OnInput(_) => "",
|
||||
@@ -355,6 +386,7 @@ pub fn render<M>(html: Html<M>): String =
|
||||
}
|
||||
},
|
||||
Text(content) => escapeHtml(content),
|
||||
RawHtml(content) => content,
|
||||
Empty => ""
|
||||
}
|
||||
|
||||
@@ -368,15 +400,47 @@ pub fn escapeHtml(s: String): String = {
|
||||
s4
|
||||
}
|
||||
|
||||
// Render a full HTML document
|
||||
// Render a full HTML document (basic)
|
||||
pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
|
||||
let headElements = List.concat([
|
||||
[Element("meta", [DataAttr("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("meta", [Attribute("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("title", [], [Text(title)])],
|
||||
headExtra
|
||||
])
|
||||
let doc = Element("html", [DataAttr("lang", "en")], [
|
||||
let doc = Element("html", [Attribute("lang", "en")], [
|
||||
Element("head", [], headElements),
|
||||
Element("body", [], bodyContent)
|
||||
])
|
||||
"<!DOCTYPE html>\n" + render(doc)
|
||||
}
|
||||
|
||||
// Render a full HTML document with SEO meta tags
|
||||
pub fn seoDocument(
|
||||
title: String,
|
||||
description: String,
|
||||
url: String,
|
||||
ogImage: String,
|
||||
headExtra: List<Html<M>>,
|
||||
bodyContent: List<Html<M>>
|
||||
): String = {
|
||||
let headElements = List.concat([
|
||||
[Element("meta", [Attribute("charset", "UTF-8")], [])],
|
||||
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
|
||||
[Element("title", [], [Text(title)])],
|
||||
[Element("meta", [Name("description"), Attribute("content", description)], [])],
|
||||
[Element("meta", [Attribute("property", "og:title"), Attribute("content", title)], [])],
|
||||
[Element("meta", [Attribute("property", "og:description"), Attribute("content", description)], [])],
|
||||
[Element("meta", [Attribute("property", "og:type"), Attribute("content", "website")], [])],
|
||||
[Element("meta", [Attribute("property", "og:url"), Attribute("content", url)], [])],
|
||||
[Element("meta", [Attribute("property", "og:image"), Attribute("content", ogImage)], [])],
|
||||
[Element("meta", [Name("twitter:card"), Attribute("content", "summary_large_image")], [])],
|
||||
[Element("meta", [Name("twitter:title"), Attribute("content", title)], [])],
|
||||
[Element("meta", [Name("twitter:description"), Attribute("content", description)], [])],
|
||||
[Element("link", [Attribute("rel", "canonical"), Href(url)], [])],
|
||||
headExtra
|
||||
])
|
||||
let doc = Element("html", [Attribute("lang", "en")], [
|
||||
Element("head", [], headElements),
|
||||
Element("body", [], bodyContent)
|
||||
])
|
||||
|
||||
@@ -625,6 +625,41 @@ pub fn router(routes: List<Route>, notFound: fn(Request): Response): Handler =
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Static File Serving
|
||||
// ============================================================
|
||||
|
||||
// Serve a static file from disk
|
||||
pub fn serveStaticFile(basePath: String, requestPath: String): Response with {File} = {
|
||||
let filePath = basePath + requestPath
|
||||
if File.exists(filePath) then {
|
||||
let content = File.read(filePath)
|
||||
let mime = getMimeType(filePath)
|
||||
{ status: 200, headers: [("Content-Type", mime)], body: content }
|
||||
} else
|
||||
{ status: 404, headers: textHeaders(), body: "Not Found" }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Form Body Parsing
|
||||
// ============================================================
|
||||
|
||||
// Parse URL-encoded form body (same format as query strings)
|
||||
pub fn parseFormBody(body: String): List<(String, String)> =
|
||||
parseQueryParams(body)
|
||||
|
||||
// Get a form field value by name
|
||||
pub fn getFormField(fields: List<(String, String)>, name: String): Option<String> =
|
||||
getParam(fields, name)
|
||||
|
||||
// ============================================================
|
||||
// Response Helpers
|
||||
// ============================================================
|
||||
|
||||
// Send a Response using HttpServer effect (convenience wrapper)
|
||||
pub fn sendResponse(resp: Response): Unit with {HttpServer} =
|
||||
HttpServer.respondWithHeaders(resp.status, resp.body, resp.headers)
|
||||
|
||||
// ============================================================
|
||||
// Example Usage
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user