diff --git a/crates/decodal-cli/src/main.rs b/crates/decodal-cli/src/main.rs index f040243..525868e 100644 --- a/crates/decodal-cli/src/main.rs +++ b/crates/decodal-cli/src/main.rs @@ -6,7 +6,7 @@ use std::{ use decodal::{ Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span, - format_diagnostic_with, + format_diagnostic_with, format_source_with_source_id, }; fn main() -> ExitCode { @@ -38,6 +38,17 @@ fn run() -> Result<(), String> { println!("ok"); Ok(()) } + "fmt" | "format" => { + let next = args.next().unwrap_or_else(|| String::from("-")); + if next == "--write" || next == "-w" { + let path = args + .next() + .ok_or_else(|| String::from("missing file path for `decodal fmt --write`"))?; + format_path(&path, true) + } else { + format_path(&next, false) + } + } "eval" | "materialize" => { let path = args.next().unwrap_or_else(|| String::from("-")); let data = materialize_path(&path)?; @@ -54,6 +65,21 @@ fn run() -> Result<(), String> { } } +fn format_path(path: &str, write: bool) -> Result<(), String> { + if write && path == "-" { + return Err(String::from("cannot use --write with stdin")); + } + let root = read_root_source(path).map_err(format_raw_diagnostic)?; + let formatted = format_source_with_source_id(SourceId(0), &root.source) + .map_err(|error| format_diagnostic_with_root(&error, &root.name))?; + if write { + fs::write(path, formatted).map_err(|error| format!("failed to write `{path}`: {error}"))?; + } else { + print!("{formatted}"); + } + Ok(()) +} + fn materialize_path(path: &str) -> Result { let root = read_root_source(path).map_err(format_raw_diagnostic)?; let root_name = root.name.clone(); @@ -143,10 +169,11 @@ fn print_help() { println!("Decodal - Deferred Constraint Data Language"); println!(); println!("Usage:"); - println!(" decodal Materialize a DCDL file"); - println!(" decodal eval Materialize a DCDL file"); - println!(" decodal check Evaluate and materialize without printing data"); - println!(" decodal - Read DCDL source from stdin"); + println!(" decodal Materialize a DCDL file"); + println!(" decodal eval Materialize a DCDL file"); + println!(" decodal check Evaluate and materialize without printing data"); + println!(" decodal fmt [--write] Format a DCDL file"); + println!(" decodal - Read DCDL source from stdin"); } fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String { diff --git a/crates/decodal-core/src/formatter.rs b/crates/decodal-core/src/formatter.rs new file mode 100644 index 0000000..321d5d1 --- /dev/null +++ b/crates/decodal-core/src/formatter.rs @@ -0,0 +1,442 @@ +use alloc::string::{String, ToString}; +use core::fmt::Write; + +use crate::{ + Ast, Diagnostic, Expr, ExprId, Field, Literal, Param, SourceForm, Span, + ast::{BinaryOp, CompareOp, MatchArm, UnaryOp}, + diagnostic::Result, + parse_source_with_source_id, + span::SourceId, +}; + +const INDENT: usize = 4; + +pub fn format_source(source: &str) -> Result { + format_source_with_source_id(SourceId(0), source) +} + +pub fn format_source_with_source_id(source_id: SourceId, source: &str) -> Result { + reject_comments(source_id, source)?; + let parsed = parse_source_with_source_id(source_id, source)?; + Ok(Formatter::new(&parsed.ast).format_root(parsed.root, parsed.source_form)) +} + +struct Formatter<'a> { + ast: &'a Ast, +} + +impl<'a> Formatter<'a> { + fn new(ast: &'a Ast) -> Self { + Self { ast } + } + + fn format_root(&self, root: ExprId, source_form: SourceForm) -> String { + let mut out = String::new(); + match source_form { + SourceForm::Fields => { + if let Expr::Object(fields) = &self.ast.get(root).expr { + self.write_fields(&mut out, fields, 0); + } else { + self.write_expr(&mut out, root, 0, 0); + } + } + SourceForm::Expr => self.write_expr(&mut out, root, 0, 0), + } + if !out.ends_with('\n') { + out.push('\n'); + } + out + } + + fn write_expr(&self, out: &mut String, id: ExprId, indent: usize, parent_prec: u8) { + let expr = &self.ast.get(id).expr; + let prec = expr_prec(expr); + let paren = prec < parent_prec; + if paren { + out.push('('); + } + match expr { + Expr::Literal(literal) => self.write_literal(out, literal), + Expr::Ident(name) => out.push_str(name), + Expr::Object(fields) => self.write_object(out, fields, indent), + Expr::Array(items) => self.write_array(out, items, indent), + Expr::Let { bindings, body } => self.write_let(out, bindings, *body, indent), + Expr::Import(path) => { + out.push_str("import "); + write_quoted(out, path); + } + Expr::Path { base, field } => { + self.write_expr(out, *base, indent, prec); + out.push('.'); + out.push_str(field); + } + Expr::Call { callee, args } => { + self.write_expr(out, *callee, indent, prec); + out.push('('); + for (index, arg) in args.iter().enumerate() { + if index > 0 { + out.push_str(", "); + } + self.write_expr(out, *arg, indent, 0); + } + out.push(')'); + } + Expr::Function { params, body } => self.write_function(out, params, *body, indent), + Expr::Match { scrutinee, arms } => self.write_match(out, *scrutinee, arms, indent), + Expr::Unary { op, expr } => { + out.push_str(match op { + UnaryOp::Neg => "-", + UnaryOp::Not => "!", + }); + self.write_expr(out, *expr, indent, prec); + } + Expr::Binary { op, lhs, rhs } => { + self.write_expr(out, *lhs, indent, prec); + out.push(' '); + out.push_str(binary_op_text(*op)); + out.push(' '); + self.write_expr(out, *rhs, indent, prec + 1); + } + Expr::Default { base, fallback } => { + self.write_expr(out, *base, indent, prec); + out.push_str(" default "); + self.write_expr(out, *fallback, indent, prec + 1); + } + Expr::CompareConstraint { op, value } => { + out.push_str(compare_op_text(*op)); + out.push(' '); + self.write_expr(out, *value, indent, prec); + } + Expr::RegexConstraint(pattern) => { + out.push('/'); + out.push_str(pattern); + out.push('/'); + } + Expr::Wildcard => out.push('_'), + } + if paren { + out.push(')'); + } + } + + fn write_literal(&self, out: &mut String, literal: &Literal) { + match literal { + Literal::String(value) => write_quoted(out, value), + Literal::Int(value) => { + let _ = write!(out, "{value}"); + } + Literal::Float(value) => { + let text = value.to_string(); + out.push_str(&text); + if !text.contains('.') && !text.contains('e') && !text.contains('E') { + out.push_str(".0"); + } + } + Literal::Bool(value) => out.push_str(if *value { "true" } else { "false" }), + } + } + + fn write_object(&self, out: &mut String, fields: &[Field], indent: usize) { + if fields.is_empty() { + out.push_str("{}"); + return; + } + out.push('{'); + out.push('\n'); + self.write_fields(out, fields, indent + INDENT); + write_indent(out, indent); + out.push('}'); + } + + fn write_fields(&self, out: &mut String, fields: &[Field], indent: usize) { + for field in fields { + write_indent(out, indent); + self.write_field(out, field, indent); + out.push_str(";\n"); + } + } + + fn write_field(&self, out: &mut String, field: &Field, indent: usize) { + for (index, part) in field.path.iter().enumerate() { + if index > 0 { + out.push('.'); + } + out.push_str(part); + } + out.push_str(" = "); + self.write_expr(out, field.value, indent, 0); + } + + fn write_array(&self, out: &mut String, items: &[ExprId], indent: usize) { + if items.is_empty() { + out.push_str("[]"); + return; + } + if items.iter().all(|item| self.is_inline(*item)) { + out.push('['); + for (index, item) in items.iter().enumerate() { + if index > 0 { + out.push_str(", "); + } + self.write_expr(out, *item, indent, 0); + } + out.push(']'); + return; + } + out.push('['); + out.push('\n'); + for item in items { + write_indent(out, indent + INDENT); + self.write_expr(out, *item, indent + INDENT, 0); + out.push_str(",\n"); + } + write_indent(out, indent); + out.push(']'); + } + + fn write_let(&self, out: &mut String, bindings: &[Field], body: ExprId, indent: usize) { + out.push_str("let"); + out.push('\n'); + self.write_fields(out, bindings, indent + INDENT); + write_indent(out, indent); + out.push_str("in"); + out.push('\n'); + write_indent(out, indent + INDENT); + self.write_expr(out, body, indent + INDENT, 0); + } + + fn write_function(&self, out: &mut String, params: &[Param], body: ExprId, indent: usize) { + out.push('('); + for (index, param) in params.iter().enumerate() { + if index > 0 { + out.push_str(", "); + } + self.write_param(out, param, indent); + } + out.push(')'); + out.push_str(" =>"); + if self.is_inline(body) { + out.push(' '); + self.write_expr(out, body, indent, 0); + } else { + out.push('\n'); + write_indent(out, indent + INDENT); + self.write_expr(out, body, indent + INDENT, 0); + } + } + + fn write_param(&self, out: &mut String, param: &Param, indent: usize) { + out.push_str(¶m.name); + if let Some(constraint) = param.constraint { + out.push_str(": "); + self.write_expr(out, constraint, indent, 0); + } + } + + fn write_match(&self, out: &mut String, scrutinee: ExprId, arms: &[MatchArm], indent: usize) { + out.push_str("match "); + self.write_expr(out, scrutinee, indent, 0); + out.push_str(" {"); + if !arms.is_empty() { + out.push('\n'); + for arm in arms { + write_indent(out, indent + INDENT); + self.write_expr(out, arm.pattern, indent + INDENT, 0); + out.push_str(": "); + self.write_expr(out, arm.body, indent + INDENT, 0); + out.push_str(";\n"); + } + write_indent(out, indent); + } + out.push('}'); + } + + fn is_inline(&self, id: ExprId) -> bool { + match &self.ast.get(id).expr { + Expr::Literal(_) + | Expr::Ident(_) + | Expr::Import(_) + | Expr::RegexConstraint(_) + | Expr::Wildcard => true, + Expr::Path { base, .. } => self.is_inline(*base), + Expr::Call { callee, args } => { + self.is_inline(*callee) && args.iter().all(|arg| self.is_inline(*arg)) + } + Expr::Unary { expr, .. } => self.is_inline(*expr), + Expr::Binary { lhs, rhs, .. } => self.is_inline(*lhs) && self.is_inline(*rhs), + Expr::Default { base, fallback } => self.is_inline(*base) && self.is_inline(*fallback), + Expr::CompareConstraint { value, .. } => self.is_inline(*value), + Expr::Array(items) => items.iter().all(|item| self.is_inline(*item)), + Expr::Object(fields) => fields.is_empty(), + Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => false, + } + } +} + +fn expr_prec(expr: &Expr) -> u8 { + match expr { + Expr::Default { .. } => 1, + Expr::Binary { op, .. } => binary_prec(*op), + Expr::Unary { .. } | Expr::CompareConstraint { .. } => 17, + Expr::Path { .. } | Expr::Call { .. } => 19, + _ => 20, + } +} + +fn binary_prec(op: BinaryOp) -> u8 { + match op { + BinaryOp::Patch => 3, + BinaryOp::And => 5, + BinaryOp::LogicalOr => 7, + BinaryOp::LogicalAnd => 9, + BinaryOp::Equal + | BinaryOp::NotEqual + | BinaryOp::Greater + | BinaryOp::GreaterEqual + | BinaryOp::Less + | BinaryOp::LessEqual => 11, + BinaryOp::Concat => 12, + BinaryOp::Add | BinaryOp::Sub => 13, + BinaryOp::Mul | BinaryOp::Div => 15, + } +} + +fn binary_op_text(op: BinaryOp) -> &'static str { + match op { + BinaryOp::Add => "+", + BinaryOp::Sub => "-", + BinaryOp::Mul => "*", + BinaryOp::Div => "/", + BinaryOp::Concat => "++", + BinaryOp::Equal => "==", + BinaryOp::NotEqual => "!=", + BinaryOp::Greater => ">", + BinaryOp::GreaterEqual => ">=", + BinaryOp::Less => "<", + BinaryOp::LessEqual => "<=", + BinaryOp::LogicalAnd => "&&", + BinaryOp::LogicalOr => "||", + BinaryOp::And => "&", + BinaryOp::Patch => "//", + } +} + +fn compare_op_text(op: CompareOp) -> &'static str { + match op { + CompareOp::Gt => ">", + CompareOp::Gte => ">=", + CompareOp::Lt => "<", + CompareOp::Lte => "<=", + CompareOp::Eq => "==", + } +} + +fn write_indent(out: &mut String, indent: usize) { + for _ in 0..indent { + out.push(' '); + } +} + +fn write_quoted(out: &mut String, value: &str) { + out.push('"'); + for ch in value.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + ch if ch.is_control() => { + let _ = write!(out, "\\u{:04x}", ch as u32); + } + ch => out.push(ch), + } + } + out.push('"'); +} + +fn reject_comments(source_id: SourceId, source: &str) -> Result<()> { + if let Some(offset) = comment_offset(source) { + return Err(Diagnostic::syntax( + Span::new(source_id, offset, offset + 1), + "formatter does not yet preserve comments", + )); + } + Ok(()) +} + +fn comment_offset(source: &str) -> Option { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'"' => { + index += 1; + while index < bytes.len() { + match bytes[index] { + b'\\' => index += 2, + b'"' => { + index += 1; + break; + } + _ => index += 1, + } + } + } + b'#' => return Some(index), + _ => index += 1, + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse_source; + + #[test] + fn formats_top_level_fields() { + let formatted = format_source("port=Int&>=1 default 8080;name=String;").unwrap(); + assert_eq!( + formatted, + "port = Int & >= 1 default 8080;\nname = String;\n" + ); + parse_source(&formatted).unwrap(); + } + + #[test] + fn formats_nested_objects() { + let formatted = + format_source("Server={host=String default \"localhost\";port=Int&>0;};").unwrap(); + assert_eq!( + formatted, + "Server = {\n host = String default \"localhost\";\n port = Int & > 0;\n};\n" + ); + parse_source(&formatted).unwrap(); + } + + #[test] + fn formats_let_function_match_and_array() { + let source = "let f=(x:Int)=>match x{_: [1,{a=2;}];};in f(1)"; + let formatted = format_source(source).unwrap(); + assert_eq!( + formatted, + "let\n f = (x: Int) =>\n match x {\n _: [\n 1,\n {\n a = 2;\n },\n ];\n };\nin\n f(1)\n" + ); + parse_source(&formatted).unwrap(); + } + + #[test] + fn preserves_needed_parentheses() { + let formatted = format_source("value=(1+2)*3;").unwrap(); + assert_eq!(formatted, "value = (1 + 2) * 3;\n"); + parse_source(&formatted).unwrap(); + } + + #[test] + fn rejects_comments_instead_of_dropping_them() { + let error = format_source("# keep me\nvalue = 1;").unwrap_err(); + assert_eq!(error.message, "formatter does not yet preserve comments"); + } +} diff --git a/crates/decodal-core/src/lib.rs b/crates/decodal-core/src/lib.rs index 27900be..ce16e85 100644 --- a/crates/decodal-core/src/lib.rs +++ b/crates/decodal-core/src/lib.rs @@ -7,6 +7,7 @@ pub mod constraints; pub mod diagnostic; pub mod embedding; pub mod eval; +pub mod formatter; mod lexer; pub mod module; pub mod parser; @@ -21,6 +22,7 @@ pub use decodal_derive::Decodal; pub use diagnostic::{Diagnostic, DiagnosticKind, Result}; pub use embedding::{HostField, HostValue}; pub use eval::{Engine, format_diagnostic_with}; +pub use formatter::{format_source, format_source_with_source_id}; pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader}; pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id}; pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue}; diff --git a/crates/decodal-wasm/src/lib.rs b/crates/decodal-wasm/src/lib.rs index 1c5f7af..31df9ab 100644 --- a/crates/decodal-wasm/src/lib.rs +++ b/crates/decodal-wasm/src/lib.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use decodal::{ Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader, - Span, format_diagnostic_with, + Span, format_diagnostic_with, format_source, }; use wasm_bindgen::prelude::*; @@ -11,6 +11,17 @@ pub fn evaluate(source: &str) -> String { encode_result(evaluate_inner(source)) } +#[wasm_bindgen(js_name = formatSource)] +pub fn format_source_json(source: &str) -> String { + match format_source(source) { + Ok(formatted) => format!("{{\"ok\":true,\"source\":{}}}", json_string(&formatted)), + Err(error) => format!( + "{{\"ok\":false,\"error\":{}}}", + json_string(&format_diagnostic_with_root(&error, "playground")) + ), + } +} + #[wasm_bindgen(js_name = evaluateProject)] pub fn evaluate_project(entry: &str, files_json: &str) -> String { encode_result(evaluate_project_inner(entry, files_json)) @@ -214,6 +225,13 @@ fn json_string(value: &str) -> String { mod tests { use super::{evaluate_project_inner, normalize_path, resolve_import}; + #[test] + fn formats_source_for_web() { + let output = super::format_source_json("value={a=1;};"); + assert!(output.contains("\"ok\":true")); + assert!(output.contains("value = {\\n a = 1;\\n};\\n")); + } + #[test] fn normalizes_virtual_paths() { assert_eq!( diff --git a/doc/manual/souce/components.md b/doc/manual/souce/components.md index 83f50b3..c1f5a9c 100644 --- a/doc/manual/souce/components.md +++ b/doc/manual/souce/components.md @@ -8,7 +8,7 @@ The public surface is intentionally organized by use case: execute Decodal with ### Rust crate The `decodal` crate is the primary Rust runtime and embedding API. -It owns parsing, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits. +It owns parsing, formatting, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits. Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program. Important paths: diff --git a/doc/manual/souce/formatting.md b/doc/manual/souce/formatting.md new file mode 100644 index 0000000..623e4ac --- /dev/null +++ b/doc/manual/souce/formatting.md @@ -0,0 +1,50 @@ +# Formatting + +Decodal includes an initial source formatter for parsed Decodal code. +The formatter is available from the Rust crate, the CLI, the WebAssembly package, and the playground. + +## CLI + +Print formatted source to stdout: + +```sh +decodal fmt config.dcdl +``` + +Format in place: + +```sh +decodal fmt --write config.dcdl +``` + +Read from stdin: + +```sh +cat config.dcdl | decodal fmt - +``` + +## Rust + +```rust +let formatted = decodal::format_source("Server={port=Int default 8080;};")?; +``` + +## WebAssembly + +```js +import init, { formatSource } from 'decodal-wasm'; + +await init(); +const result = JSON.parse(formatSource('Server={port=Int default 8080;};')); + +if (result.ok) { + console.log(result.source); +} else { + console.error(result.error); +} +``` + +## Current limitation + +The formatter currently rejects source containing line comments rather than silently dropping them. +Comment-preserving formatting requires attaching comments to syntax nodes or formatting from a concrete syntax tree, and should be implemented before comment-heavy source is formatted automatically. diff --git a/doc/manual/souce/index.md b/doc/manual/souce/index.md index b4a1323..d17bc4d 100644 --- a/doc/manual/souce/index.md +++ b/doc/manual/souce/index.md @@ -7,7 +7,8 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク 1. [Introduction](./introduction.md) 2. [Components](./components.md) -3. [Language Specification](./language/index.md) +3. [Formatting](./formatting.md) +4. [Language Specification](./language/index.md) 1. [Lexical Structure and Syntax](./language/syntax.md) 2. [Value](./language/value/index.md) 1. [String](./language/value/string.md) @@ -36,7 +37,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク 9. [Materialization and Errors](./language/materialization-and-errors.md) 10. [Naming Conventions](./language/naming.md) 11. [Examples](./language/examples.md) -4. [Implementation Design](./design/index.md) +5. [Implementation Design](./design/index.md) 1. [Execution Pipeline](./design/execution-pipeline.md) 2. [Runtime Model](./design/runtime-model.md) 3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md) @@ -44,5 +45,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク 5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md) 6. [Embedding API](./design/embedding-api.md) 7. [Features](./design/features.md) -5. [Development](./development.md) -6. [Open Issues](./open-issues.md) +6. [Development](./development.md) +7. [Open Issues](./open-issues.md) diff --git a/packages/decodal-wasm/README.md b/packages/decodal-wasm/README.md index 7a33125..72cb7b2 100644 --- a/packages/decodal-wasm/README.md +++ b/packages/decodal-wasm/README.md @@ -22,15 +22,15 @@ deno add jsr:@hare/decodal-wasm ## Usage ```js -import init, { evaluateProject } from 'decodal-wasm'; +import init, { evaluateProject, formatSource } from 'decodal-wasm'; await init(); -const result = evaluateProject(JSON.stringify({ - entry: 'main.dcdl', - files: { - 'main.dcdl': 'Server = { port = Int default 8080; };', - }, +const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};')); +console.log(formatted.source); + +const result = evaluateProject('main.dcdl', JSON.stringify({ + 'main.dcdl': 'Server = { port = Int default 8080; };', })); console.log(JSON.parse(result)); diff --git a/packages/decodal-wasm/decodal_wasm.d.ts b/packages/decodal-wasm/decodal_wasm.d.ts index e88b72f..e56f72d 100644 --- a/packages/decodal-wasm/decodal_wasm.d.ts +++ b/packages/decodal-wasm/decodal_wasm.d.ts @@ -5,12 +5,15 @@ export function evaluate(source: string): string; export function evaluateProject(entry: string, files_json: string): string; +export function formatSource(source: string): string; + export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; readonly evaluate: (a: number, b: number) => [number, number]; readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number]; + readonly formatSource: (a: number, b: number) => [number, number]; readonly __wbindgen_externrefs: WebAssembly.Table; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; diff --git a/packages/decodal-wasm/decodal_wasm.js b/packages/decodal-wasm/decodal_wasm.js index 800a76f..fe597bd 100644 --- a/packages/decodal-wasm/decodal_wasm.js +++ b/packages/decodal-wasm/decodal_wasm.js @@ -40,6 +40,25 @@ export function evaluateProject(entry, files_json) { wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); } } + +/** + * @param {string} source + * @returns {string} + */ +export function formatSource(source) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.formatSource(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} function __wbg_get_imports() { const import0 = { __proto__: null, diff --git a/packages/decodal-wasm/decodal_wasm_bg.wasm b/packages/decodal-wasm/decodal_wasm_bg.wasm index c03a15e..5a6df24 100644 Binary files a/packages/decodal-wasm/decodal_wasm_bg.wasm and b/packages/decodal-wasm/decodal_wasm_bg.wasm differ diff --git a/packages/decodal-wasm/decodal_wasm_bg.wasm.d.ts b/packages/decodal-wasm/decodal_wasm_bg.wasm.d.ts index 4f894cc..3c35b37 100644 --- a/packages/decodal-wasm/decodal_wasm_bg.wasm.d.ts +++ b/packages/decodal-wasm/decodal_wasm_bg.wasm.d.ts @@ -3,6 +3,7 @@ export const memory: WebAssembly.Memory; export const evaluate: (a: number, b: number) => [number, number]; export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number]; +export const formatSource: (a: number, b: number) => [number, number]; export const __wbindgen_externrefs: WebAssembly.Table; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; diff --git a/site/decodal-site/scripts/prepare-wasm-package.mjs b/site/decodal-site/scripts/prepare-wasm-package.mjs index 250aae1..d7ffe10 100644 --- a/site/decodal-site/scripts/prepare-wasm-package.mjs +++ b/site/decodal-site/scripts/prepare-wasm-package.mjs @@ -34,15 +34,15 @@ deno add jsr:@hare/decodal-wasm ## Usage \`\`\`js -import init, { evaluateProject } from 'decodal-wasm'; +import init, { evaluateProject, formatSource } from 'decodal-wasm'; await init(); -const result = evaluateProject(JSON.stringify({ - entry: 'main.dcdl', - files: { - 'main.dcdl': 'Server = { port = Int default 8080; };', - }, +const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};')); +console.log(formatted.source); + +const result = evaluateProject('main.dcdl', JSON.stringify({ + 'main.dcdl': 'Server = { port = Int default 8080; };', })); console.log(JSON.parse(result)); diff --git a/site/decodal-site/src/lib/docs.js b/site/decodal-site/src/lib/docs.js index ba791d9..6be6cd1 100644 --- a/site/decodal-site/src/lib/docs.js +++ b/site/decodal-site/src/lib/docs.js @@ -20,6 +20,7 @@ export const docs = Object.fromEntries( export const nav = [ { title: 'Introduction', slug: 'introduction' }, { title: 'Components', slug: 'components' }, + { title: 'Formatting', slug: 'formatting' }, { title: 'Language Specification', slug: 'language', diff --git a/site/decodal-site/src/pages/playground.astro b/site/decodal-site/src/pages/playground.astro index 09af8ab..1a179a5 100644 --- a/site/decodal-site/src/pages/playground.astro +++ b/site/decodal-site/src/pages/playground.astro @@ -15,6 +15,7 @@ import ManualLayout from '../layouts/ManualLayout.astro';

Loading WASM...

+ diff --git a/site/decodal-site/src/scripts/playground.js b/site/decodal-site/src/scripts/playground.js index 40c3447..a613b6b 100644 --- a/site/decodal-site/src/scripts/playground.js +++ b/site/decodal-site/src/scripts/playground.js @@ -1,4 +1,4 @@ -import init, { evaluateProject } from 'decodal-wasm'; +import init, { evaluateProject, formatSource } from 'decodal-wasm'; import { EditorView, basicSetup } from 'codemirror'; import { keymap } from '@codemirror/view'; import { decodal } from 'decodal-codemirror'; @@ -10,6 +10,7 @@ const starterProject = playgroundExamples[0]; const editorHost = document.getElementById('editor'); const output = document.getElementById('output'); const run = document.getElementById('run'); +const formatButton = document.getElementById('format'); const status = document.getElementById('status'); const fileTree = document.getElementById('file-tree'); const activeFile = document.getElementById('active-file'); @@ -176,6 +177,20 @@ function execute() { output.classList.toggle('error', !result.ok); } +function formatActiveFile() { + const result = JSON.parse(formatSource(getEditorText())); + if (!result.ok) { + output.textContent = result.error; + output.classList.add('error'); + return; + } + setEditorText(result.source); + project.files[project.activePath] = result.source; + saveProject(); + output.textContent = 'Formatted.'; + output.classList.remove('error'); +} + function renderFileTree() { const tree = buildTree(Object.keys(project.files).sort()); fileTree.replaceChildren(renderTreeList(tree.children)); @@ -229,6 +244,7 @@ function compareNodes(a, b) { try { await init(); run.disabled = false; + formatButton.disabled = false; status.textContent = ''; execute(); } catch (error) { @@ -236,6 +252,7 @@ try { } run.addEventListener('click', execute); +formatButton.addEventListener('click', formatActiveFile); loadExample.addEventListener('click', () => { const example = playgroundExamples.find((item) => item.id === exampleSelect.value); if (!example) return;