feat: rebuild website with full learning funnel
Website rebuilt from scratch based on analysis of 11 beloved language websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc, Rust, Go). New website structure: - Homepage with hero, playground, three pillars, install guide - Language Tour with interactive lessons (hello world, types, effects) - Examples cookbook with categorized sidebar - API documentation index - Installation guide (Nix and source) - Sleek/noble design (black/gold, serif typography) Also includes: - New stdlib/json.lux module for JSON serialization - Enhanced stdlib/http.lux with middleware and routing - New string functions (charAt, indexOf, lastIndexOf, repeat) - LSP improvements (rename, signature help, formatting) - Package manager transitive dependency resolution - Updated documentation for effects and stdlib - New showcase example (task_manager.lux) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
733
src/main.rs
733
src/main.rs
@@ -15,6 +15,7 @@ mod package;
|
||||
mod parser;
|
||||
mod registry;
|
||||
mod schema;
|
||||
mod symbol_table;
|
||||
mod typechecker;
|
||||
mod types;
|
||||
|
||||
@@ -47,8 +48,10 @@ Commands:
|
||||
:env Show user-defined bindings
|
||||
:clear Clear the environment
|
||||
:load <file> Load and execute a file
|
||||
:reload, :r Reload the last loaded file
|
||||
:trace on/off Enable/disable effect tracing
|
||||
:traces Show recorded effect traces
|
||||
:ast <expr> Show the AST of an expression (for debugging)
|
||||
|
||||
Keyboard:
|
||||
Tab Autocomplete
|
||||
@@ -57,6 +60,10 @@ Keyboard:
|
||||
Up/Down Browse history
|
||||
Ctrl-R Search history
|
||||
|
||||
Effects:
|
||||
All code in the REPL runs with Console, File, and other standard effects.
|
||||
Use :trace on to see effect invocations during execution.
|
||||
|
||||
Examples:
|
||||
> let x = 42
|
||||
> x + 1
|
||||
@@ -65,6 +72,9 @@ Examples:
|
||||
> fn double(n: Int): Int = n * 2
|
||||
> :type double
|
||||
double : fn(Int) -> Int
|
||||
|
||||
> :load myfile.lux
|
||||
> :reload
|
||||
> double(21)
|
||||
42
|
||||
|
||||
@@ -175,6 +185,10 @@ fn main() {
|
||||
compile_to_c(&args[2], output_path, run_after, emit_c);
|
||||
}
|
||||
}
|
||||
"doc" => {
|
||||
// Generate API documentation
|
||||
generate_docs(&args[2..]);
|
||||
}
|
||||
path => {
|
||||
// Run a file
|
||||
run_file(path);
|
||||
@@ -207,6 +221,8 @@ fn print_help() {
|
||||
println!(" lux registry Start package registry server");
|
||||
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
|
||||
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
|
||||
println!(" lux doc [file] [-o dir] Generate API documentation (HTML)");
|
||||
println!(" --json Output as JSON");
|
||||
println!(" lux --lsp Start LSP server (for IDE integration)");
|
||||
println!(" lux --help Show this help");
|
||||
println!(" lux --version Show version");
|
||||
@@ -1428,6 +1444,681 @@ let output = run main() with {}
|
||||
println!(" lux src/main.lux");
|
||||
}
|
||||
|
||||
/// Generate API documentation for Lux source files
|
||||
fn generate_docs(args: &[String]) {
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let output_json = args.iter().any(|a| a == "--json");
|
||||
let output_dir = args.iter()
|
||||
.position(|a| a == "-o")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("docs");
|
||||
let input_file = args.iter().find(|a| !a.starts_with('-') && *a != output_dir);
|
||||
|
||||
// Collect files to document
|
||||
let mut files_to_doc = Vec::new();
|
||||
|
||||
if let Some(path) = input_file {
|
||||
if Path::new(path).is_file() {
|
||||
files_to_doc.push(path.to_string());
|
||||
} else {
|
||||
eprintln!("File not found: {}", path);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
// Auto-discover files
|
||||
if Path::new("src").is_dir() {
|
||||
collect_lux_files_for_docs("src", &mut files_to_doc);
|
||||
}
|
||||
if Path::new("stdlib").is_dir() {
|
||||
collect_lux_files_for_docs("stdlib", &mut files_to_doc);
|
||||
}
|
||||
}
|
||||
|
||||
if files_to_doc.is_empty() {
|
||||
eprintln!("No .lux files found to document");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
if !output_json {
|
||||
if let Err(e) = std::fs::create_dir_all(output_dir) {
|
||||
eprintln!("Failed to create output directory: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_docs: HashMap<String, ModuleDoc> = HashMap::new();
|
||||
let mut error_count = 0;
|
||||
|
||||
for file_path in &files_to_doc {
|
||||
let source = match std::fs::read_to_string(file_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("{}: ERROR - {}", file_path, e);
|
||||
error_count += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match extract_module_doc(&source, file_path) {
|
||||
Ok(doc) => {
|
||||
let module_name = Path::new(file_path)
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
all_docs.insert(module_name, doc);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: PARSE ERROR - {}", file_path, e);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if output_json {
|
||||
// Output as JSON
|
||||
println!("{}", docs_to_json(&all_docs));
|
||||
} else {
|
||||
// Generate HTML files
|
||||
let index_html = generate_index_html(&all_docs);
|
||||
let index_path = format!("{}/index.html", output_dir);
|
||||
if let Err(e) = std::fs::write(&index_path, &index_html) {
|
||||
eprintln!("Failed to write index.html: {}", e);
|
||||
error_count += 1;
|
||||
}
|
||||
|
||||
for (module_name, doc) in &all_docs {
|
||||
let html = generate_module_html(module_name, doc);
|
||||
let path = format!("{}/{}.html", output_dir, module_name);
|
||||
if let Err(e) = std::fs::write(&path, &html) {
|
||||
eprintln!("Failed to write {}: {}", path, e);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate CSS
|
||||
let css_path = format!("{}/style.css", output_dir);
|
||||
if let Err(e) = std::fs::write(&css_path, DOC_CSS) {
|
||||
eprintln!("Failed to write style.css: {}", e);
|
||||
error_count += 1;
|
||||
}
|
||||
|
||||
println!("Generated documentation in {}/", output_dir);
|
||||
println!(" {} modules documented", all_docs.len());
|
||||
if error_count > 0 {
|
||||
println!(" {} errors", error_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_lux_files_for_docs(dir: &str, files: &mut Vec<String>) {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
files.push(path.to_string_lossy().to_string());
|
||||
} else if path.is_dir() {
|
||||
collect_lux_files_for_docs(&path.to_string_lossy(), files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ModuleDoc {
|
||||
description: Option<String>,
|
||||
functions: Vec<FunctionDoc>,
|
||||
types: Vec<TypeDoc>,
|
||||
effects: Vec<EffectDoc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FunctionDoc {
|
||||
name: String,
|
||||
signature: String,
|
||||
description: Option<String>,
|
||||
is_public: bool,
|
||||
properties: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TypeDoc {
|
||||
name: String,
|
||||
definition: String,
|
||||
description: Option<String>,
|
||||
is_public: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EffectDoc {
|
||||
name: String,
|
||||
operations: Vec<String>,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
|
||||
use modules::ModuleLoader;
|
||||
use std::path::Path;
|
||||
|
||||
let mut loader = ModuleLoader::new();
|
||||
let file_path = Path::new(path);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
loader.add_search_path(parent.to_path_buf());
|
||||
}
|
||||
|
||||
let program = loader.load_source(source, Some(file_path))
|
||||
.map_err(|e| format!("{}", e))?;
|
||||
|
||||
let mut module_desc: Option<String> = None;
|
||||
let mut functions = Vec::new();
|
||||
let mut types = Vec::new();
|
||||
let mut effects = Vec::new();
|
||||
let mut pending_doc: Option<String> = None;
|
||||
|
||||
// Extract module-level comment (first comment before any declarations)
|
||||
let lines: Vec<&str> = source.lines().collect();
|
||||
let mut module_comment = Vec::new();
|
||||
for line in &lines {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") {
|
||||
module_comment.push(trimmed.trim_start_matches('/').trim());
|
||||
} else if !trimmed.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !module_comment.is_empty() {
|
||||
module_desc = Some(module_comment.join("\n"));
|
||||
}
|
||||
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
ast::Declaration::Function(f) => {
|
||||
// Build signature
|
||||
let params: Vec<String> = f.params.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
|
||||
.collect();
|
||||
let effects_str = if f.effects.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" with {{{}}}", f.effects.iter().map(|e| e.name.clone()).collect::<Vec<_>>().join(", "))
|
||||
};
|
||||
let props: Vec<String> = f.properties.iter()
|
||||
.map(|p| format!("{:?}", p).to_lowercase())
|
||||
.collect();
|
||||
let props_str = if props.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" is {}", props.join(", "))
|
||||
};
|
||||
|
||||
let signature = format!(
|
||||
"fn {}({}): {}{}{}",
|
||||
f.name.name,
|
||||
params.join(", "),
|
||||
format_type(&f.return_type),
|
||||
props_str,
|
||||
effects_str
|
||||
);
|
||||
|
||||
// Extract doc comment
|
||||
let doc = extract_doc_comment(source, f.span.start);
|
||||
|
||||
functions.push(FunctionDoc {
|
||||
name: f.name.name.clone(),
|
||||
signature,
|
||||
description: doc,
|
||||
is_public: matches!(f.visibility, ast::Visibility::Public),
|
||||
properties: props,
|
||||
});
|
||||
}
|
||||
ast::Declaration::Type(t) => {
|
||||
let doc = extract_doc_comment(source, t.span.start);
|
||||
types.push(TypeDoc {
|
||||
name: t.name.name.clone(),
|
||||
definition: format_type_def(t),
|
||||
description: doc,
|
||||
is_public: matches!(t.visibility, ast::Visibility::Public),
|
||||
});
|
||||
}
|
||||
ast::Declaration::Effect(e) => {
|
||||
let doc = extract_doc_comment(source, e.span.start);
|
||||
let ops: Vec<String> = e.operations.iter()
|
||||
.map(|op| {
|
||||
let params: Vec<String> = op.params.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
|
||||
.collect();
|
||||
format!("{}({}): {}", op.name.name, params.join(", "), format_type(&op.return_type))
|
||||
})
|
||||
.collect();
|
||||
effects.push(EffectDoc {
|
||||
name: e.name.name.clone(),
|
||||
operations: ops,
|
||||
description: doc,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ModuleDoc {
|
||||
description: module_desc,
|
||||
functions,
|
||||
types,
|
||||
effects,
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_doc_comment(source: &str, pos: usize) -> Option<String> {
|
||||
// Look backwards from the declaration for doc comments
|
||||
let prefix = &source[..pos];
|
||||
let lines: Vec<&str> = prefix.lines().collect();
|
||||
|
||||
let mut doc_lines = Vec::new();
|
||||
for line in lines.iter().rev() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("///") {
|
||||
doc_lines.push(trimmed.trim_start_matches('/').trim());
|
||||
} else if trimmed.starts_with("//") {
|
||||
// Regular comment, skip
|
||||
continue;
|
||||
} else if trimmed.is_empty() {
|
||||
if !doc_lines.is_empty() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if doc_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
doc_lines.reverse();
|
||||
Some(doc_lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_type(t: &ast::TypeExpr) -> String {
|
||||
match t {
|
||||
ast::TypeExpr::Named(ident) => ident.name.clone(),
|
||||
ast::TypeExpr::App(base, args) => {
|
||||
let args_str: Vec<String> = args.iter().map(format_type).collect();
|
||||
format!("{}<{}>", format_type(base), args_str.join(", "))
|
||||
}
|
||||
ast::TypeExpr::Function { params, return_type, .. } => {
|
||||
let params_str: Vec<String> = params.iter().map(format_type).collect();
|
||||
format!("fn({}): {}", params_str.join(", "), format_type(return_type))
|
||||
}
|
||||
ast::TypeExpr::Tuple(types) => {
|
||||
let types_str: Vec<String> = types.iter().map(format_type).collect();
|
||||
format!("({})", types_str.join(", "))
|
||||
}
|
||||
ast::TypeExpr::Record(fields) => {
|
||||
let fields_str: Vec<String> = fields.iter()
|
||||
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
|
||||
.collect();
|
||||
format!("{{ {} }}", fields_str.join(", "))
|
||||
}
|
||||
ast::TypeExpr::Unit => "Unit".to_string(),
|
||||
ast::TypeExpr::Versioned { base, .. } => format_type(base),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_type_def(t: &ast::TypeDecl) -> String {
|
||||
match &t.definition {
|
||||
ast::TypeDef::Alias(typ) => format!("type {} = {}", t.name.name, format_type(typ)),
|
||||
ast::TypeDef::Enum(variants) => {
|
||||
let variants_str: Vec<String> = variants.iter()
|
||||
.map(|v| {
|
||||
match &v.fields {
|
||||
ast::VariantFields::Unit => v.name.name.clone(),
|
||||
ast::VariantFields::Tuple(types) => {
|
||||
let types_str: Vec<String> = types.iter().map(format_type).collect();
|
||||
format!("{}({})", v.name.name, types_str.join(", "))
|
||||
}
|
||||
ast::VariantFields::Record(fields) => {
|
||||
let fields_str: Vec<String> = fields.iter()
|
||||
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
|
||||
.collect();
|
||||
format!("{}{{ {} }}", v.name.name, fields_str.join(", "))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("type {} = {}", t.name.name, variants_str.join(" | "))
|
||||
}
|
||||
ast::TypeDef::Record(fields) => {
|
||||
let fields_str: Vec<String> = fields.iter()
|
||||
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
|
||||
.collect();
|
||||
format!("type {} = {{ {} }}", t.name.name, fields_str.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn docs_to_json(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
|
||||
let mut json = String::from("{\n");
|
||||
let mut first_module = true;
|
||||
|
||||
for (name, doc) in docs {
|
||||
if !first_module {
|
||||
json.push_str(",\n");
|
||||
}
|
||||
first_module = false;
|
||||
|
||||
json.push_str(&format!(" \"{}\": {{\n", escape_json(name)));
|
||||
|
||||
if let Some(desc) = &doc.description {
|
||||
json.push_str(&format!(" \"description\": \"{}\",\n", escape_json(desc)));
|
||||
}
|
||||
|
||||
// Functions
|
||||
json.push_str(" \"functions\": [\n");
|
||||
for (i, f) in doc.functions.iter().enumerate() {
|
||||
json.push_str(&format!(
|
||||
" {{\"name\": \"{}\", \"signature\": \"{}\", \"public\": {}, \"description\": {}}}",
|
||||
escape_json(&f.name),
|
||||
escape_json(&f.signature),
|
||||
f.is_public,
|
||||
f.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
|
||||
));
|
||||
if i < doc.functions.len() - 1 {
|
||||
json.push(',');
|
||||
}
|
||||
json.push('\n');
|
||||
}
|
||||
json.push_str(" ],\n");
|
||||
|
||||
// Types
|
||||
json.push_str(" \"types\": [\n");
|
||||
for (i, t) in doc.types.iter().enumerate() {
|
||||
json.push_str(&format!(
|
||||
" {{\"name\": \"{}\", \"definition\": \"{}\", \"public\": {}, \"description\": {}}}",
|
||||
escape_json(&t.name),
|
||||
escape_json(&t.definition),
|
||||
t.is_public,
|
||||
t.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
|
||||
));
|
||||
if i < doc.types.len() - 1 {
|
||||
json.push(',');
|
||||
}
|
||||
json.push('\n');
|
||||
}
|
||||
json.push_str(" ],\n");
|
||||
|
||||
// Effects
|
||||
json.push_str(" \"effects\": [\n");
|
||||
for (i, e) in doc.effects.iter().enumerate() {
|
||||
let ops_json: Vec<String> = e.operations.iter()
|
||||
.map(|o| format!("\"{}\"", escape_json(o)))
|
||||
.collect();
|
||||
json.push_str(&format!(
|
||||
" {{\"name\": \"{}\", \"operations\": [{}], \"description\": {}}}",
|
||||
escape_json(&e.name),
|
||||
ops_json.join(", "),
|
||||
e.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
|
||||
));
|
||||
if i < doc.effects.len() - 1 {
|
||||
json.push(',');
|
||||
}
|
||||
json.push('\n');
|
||||
}
|
||||
json.push_str(" ]\n");
|
||||
|
||||
json.push_str(" }");
|
||||
}
|
||||
|
||||
json.push_str("\n}");
|
||||
json
|
||||
}
|
||||
|
||||
fn escape_json(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n")
|
||||
.replace('\r', "\\r")
|
||||
.replace('\t', "\\t")
|
||||
}
|
||||
|
||||
fn generate_index_html(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
|
||||
let mut html = String::from(r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lux API Documentation</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Lux API Documentation</h1>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Modules</h2>
|
||||
<ul class="module-list">
|
||||
"#);
|
||||
|
||||
let mut modules: Vec<_> = docs.keys().collect();
|
||||
modules.sort();
|
||||
|
||||
for name in modules {
|
||||
let doc = &docs[name];
|
||||
let desc = doc.description.as_ref()
|
||||
.map(|d| d.lines().next().unwrap_or(""))
|
||||
.unwrap_or("");
|
||||
html.push_str(&format!(
|
||||
" <li><a href=\"{}.html\">{}</a> - {}</li>\n",
|
||||
name, name, html_escape(desc)
|
||||
));
|
||||
}
|
||||
|
||||
html.push_str(r#" </ul>
|
||||
</main>
|
||||
</body>
|
||||
</html>"#);
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn generate_module_html(name: &str, doc: &ModuleDoc) -> String {
|
||||
let mut html = format!(r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{} - Lux API</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="index.html">Back to Index</a>
|
||||
<h1>{}</h1>
|
||||
</header>
|
||||
<main>
|
||||
"#, name, name);
|
||||
|
||||
if let Some(desc) = &doc.description {
|
||||
html.push_str(&format!(" <div class=\"module-description\">{}</div>\n", html_escape(desc)));
|
||||
}
|
||||
|
||||
// Types
|
||||
if !doc.types.is_empty() {
|
||||
html.push_str(" <section>\n <h2>Types</h2>\n");
|
||||
for t in &doc.types {
|
||||
let visibility = if t.is_public { "pub " } else { "" };
|
||||
html.push_str(&format!(
|
||||
" <div class=\"item\">\n <code class=\"signature\">{}{}</code>\n",
|
||||
visibility, html_escape(&t.definition)
|
||||
));
|
||||
if let Some(desc) = &t.description {
|
||||
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
|
||||
}
|
||||
html.push_str(" </div>\n");
|
||||
}
|
||||
html.push_str(" </section>\n");
|
||||
}
|
||||
|
||||
// Effects
|
||||
if !doc.effects.is_empty() {
|
||||
html.push_str(" <section>\n <h2>Effects</h2>\n");
|
||||
for e in &doc.effects {
|
||||
html.push_str(&format!(
|
||||
" <div class=\"item\">\n <h3>effect {}</h3>\n",
|
||||
html_escape(&e.name)
|
||||
));
|
||||
if let Some(desc) = &e.description {
|
||||
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
|
||||
}
|
||||
html.push_str(" <ul class=\"operations\">\n");
|
||||
for op in &e.operations {
|
||||
html.push_str(&format!(" <li><code>{}</code></li>\n", html_escape(op)));
|
||||
}
|
||||
html.push_str(" </ul>\n </div>\n");
|
||||
}
|
||||
html.push_str(" </section>\n");
|
||||
}
|
||||
|
||||
// Functions
|
||||
if !doc.functions.is_empty() {
|
||||
html.push_str(" <section>\n <h2>Functions</h2>\n");
|
||||
for f in &doc.functions {
|
||||
let visibility = if f.is_public { "pub " } else { "" };
|
||||
html.push_str(&format!(
|
||||
" <div class=\"item\" id=\"{}\">\n <code class=\"signature\">{}{}</code>\n",
|
||||
f.name, visibility, html_escape(&f.signature)
|
||||
));
|
||||
if let Some(desc) = &f.description {
|
||||
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
|
||||
}
|
||||
html.push_str(" </div>\n");
|
||||
}
|
||||
html.push_str(" </section>\n");
|
||||
}
|
||||
|
||||
html.push_str(r#" </main>
|
||||
</body>
|
||||
</html>"#);
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn html_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
const DOC_CSS: &str = r#"
|
||||
:root {
|
||||
--bg-color: #1a1a2e;
|
||||
--text-color: #e0e0e0;
|
||||
--link-color: #64b5f6;
|
||||
--code-bg: #16213e;
|
||||
--header-bg: #0f3460;
|
||||
--accent: #e94560;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--header-bg);
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
header a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid var(--accent);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.module-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.module-list li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.module-list a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item {
|
||||
background-color: var(--code-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.signature {
|
||||
display: block;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', 'Monaco', monospace;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 0.5rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.operations {
|
||||
list-style: none;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.operations li {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.module-description {
|
||||
background-color: var(--code-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
"#;
|
||||
|
||||
fn run_file(path: &str) {
|
||||
use modules::ModuleLoader;
|
||||
use std::path::Path;
|
||||
@@ -1485,6 +2176,7 @@ struct LuxHelper {
|
||||
keywords: HashSet<String>,
|
||||
commands: Vec<String>,
|
||||
user_defined: HashSet<String>,
|
||||
last_loaded_file: Option<String>,
|
||||
}
|
||||
|
||||
impl LuxHelper {
|
||||
@@ -1502,7 +2194,8 @@ impl LuxHelper {
|
||||
|
||||
let commands = vec![
|
||||
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
|
||||
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
|
||||
":reload", ":r", ":trace", ":traces", ":info", ":i", ":env", ":doc", ":d",
|
||||
":browse", ":b", ":ast",
|
||||
]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
@@ -1512,6 +2205,7 @@ impl LuxHelper {
|
||||
keywords,
|
||||
commands,
|
||||
user_defined: HashSet::new(),
|
||||
last_loaded_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1860,11 +2554,31 @@ fn handle_command(
|
||||
}
|
||||
":load" | ":l" => {
|
||||
if let Some(path) = arg {
|
||||
helper.last_loaded_file = Some(path.to_string());
|
||||
load_file(path, interp, checker, helper);
|
||||
} else {
|
||||
println!("Usage: :load <filename>");
|
||||
}
|
||||
}
|
||||
":reload" | ":r" => {
|
||||
if let Some(ref path) = helper.last_loaded_file.clone() {
|
||||
println!("Reloading {}...", path);
|
||||
// Clear environment first
|
||||
*interp = Interpreter::new();
|
||||
*checker = TypeChecker::new();
|
||||
helper.user_defined.clear();
|
||||
load_file(path, interp, checker, helper);
|
||||
} else {
|
||||
println!("No file to reload. Use :load <file> first.");
|
||||
}
|
||||
}
|
||||
":ast" => {
|
||||
if let Some(expr_str) = arg {
|
||||
show_ast(expr_str);
|
||||
} else {
|
||||
println!("Usage: :ast <expression>");
|
||||
}
|
||||
}
|
||||
":trace" => match arg {
|
||||
Some("on") => {
|
||||
interp.enable_tracing();
|
||||
@@ -2163,6 +2877,23 @@ fn show_type(expr_str: &str, checker: &mut TypeChecker) {
|
||||
}
|
||||
}
|
||||
|
||||
fn show_ast(expr_str: &str) {
|
||||
// Wrap expression in a let to parse it
|
||||
let wrapped = format!("let _expr_ = {}", expr_str);
|
||||
|
||||
match Parser::parse_source(&wrapped) {
|
||||
Ok(program) => {
|
||||
// Pretty print the AST
|
||||
for decl in &program.declarations {
|
||||
println!("{:#?}", decl);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Parse error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker, helper: &mut LuxHelper) {
|
||||
let source = match std::fs::read_to_string(path) {
|
||||
Ok(s) => s,
|
||||
|
||||
Reference in New Issue
Block a user