Compare commits
23
Commits
16006d000a
...
5a05da6fe4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a05da6fe4 | ||
|
|
aa0f464320 | ||
|
|
fc1f3e0b25 | ||
|
|
8eac5c5cb4 | ||
|
|
59cb8f31e4 | ||
|
|
4eee2cd2c9 | ||
|
|
44b6041e60 | ||
|
|
a95afe0463 | ||
|
|
43ab6244ef | ||
|
|
1d4503b015 | ||
|
|
250e29e00c | ||
|
|
1080081cec | ||
|
|
43f692e75c | ||
|
|
5b26d071ca | ||
|
|
a653f0b7c6 | ||
|
|
384d17b539 | ||
|
|
09a84264ab | ||
|
|
0a8c261545 | ||
|
|
a8a46998a8 | ||
|
|
e4a9f80617 | ||
|
|
0e2a7b35d2 | ||
|
|
ac99c4cb4e | ||
|
|
22f6bc1ab9 |
Generated
+51
@@ -17,6 +17,16 @@ version = "3.20.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.66"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -48,6 +58,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-language-tools"
|
||||||
|
version = "0.1.2"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"serde_json",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-decodal",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-wasm"
|
name = "decodal-wasm"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -57,6 +78,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -170,6 +197,12 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -181,6 +214,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter"
|
||||||
|
version = "0.22.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-decodal"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ members = [
|
|||||||
"crates/decodal-cli",
|
"crates/decodal-cli",
|
||||||
"crates/decodal-wasm",
|
"crates/decodal-wasm",
|
||||||
"crates/decodal-derive",
|
"crates/decodal-derive",
|
||||||
|
"crates/decodal-language-tools",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-language-tools"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Source-level language tooling for Decodal."
|
||||||
|
keywords = ["decodal", "formatter", "lsp", "tree-sitter"]
|
||||||
|
categories = ["development-tools", "text-processing"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.1.2", path = "../decodal-core" }
|
||||||
|
serde_json.workspace = true
|
||||||
|
tree-sitter = "0.22.6"
|
||||||
|
tree-sitter-decodal = { path = "../../editors/tree-sitter-decodal" }
|
||||||
|
wasm-bindgen.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
@@ -0,0 +1,851 @@
|
|||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use tree_sitter::{Node, Parser, TreeCursor};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
const INDENT: usize = 4;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FormatError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatError {
|
||||||
|
fn new(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> &str {
|
||||||
|
&self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FormatError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for FormatError {}
|
||||||
|
|
||||||
|
pub fn format_source(source: &str) -> Result<String, FormatError> {
|
||||||
|
decodal::parse_source(source)
|
||||||
|
.map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?;
|
||||||
|
format_source_impl(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn format_source_impl(source: &str) -> Result<String, FormatError> {
|
||||||
|
let mut parser = Parser::new();
|
||||||
|
parser
|
||||||
|
.set_language(&tree_sitter_decodal::language())
|
||||||
|
.map_err(|error| {
|
||||||
|
FormatError::new(format!("failed to load tree-sitter grammar: {error}"))
|
||||||
|
})?;
|
||||||
|
let tree = parser
|
||||||
|
.parse(source, None)
|
||||||
|
.ok_or_else(|| FormatError::new("tree-sitter failed to parse source"))?;
|
||||||
|
let root = tree.root_node();
|
||||||
|
if root.has_error() {
|
||||||
|
return Err(FormatError::new(
|
||||||
|
"parse error: source contains tree-sitter errors",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut formatter = Formatter::new(source);
|
||||||
|
let mut out = formatter.format_source_file(root);
|
||||||
|
if !out.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = formatSource)]
|
||||||
|
pub fn format_source_json(source: &str) -> String {
|
||||||
|
match format_source(source) {
|
||||||
|
Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(),
|
||||||
|
Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn format_source_impl(source: &str) -> Result<String, FormatError> {
|
||||||
|
Ok(LexicalFormatter::new(source).format())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
struct LexicalFormatter<'a> {
|
||||||
|
chars: core::iter::Peekable<core::str::Chars<'a>>,
|
||||||
|
out: String,
|
||||||
|
indent: usize,
|
||||||
|
line_start: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl<'a> LexicalFormatter<'a> {
|
||||||
|
fn new(source: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
chars: source.chars().peekable(),
|
||||||
|
out: String::new(),
|
||||||
|
indent: 0,
|
||||||
|
line_start: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(mut self) -> String {
|
||||||
|
while let Some(ch) = self.chars.next() {
|
||||||
|
match ch {
|
||||||
|
' ' | '\t' | '\r' | '\n' => self.consume_whitespace(ch),
|
||||||
|
'#' => self.write_comment(),
|
||||||
|
'"' => self.write_string(),
|
||||||
|
'{' => self.open_block('{'),
|
||||||
|
'[' => self.open_block('['),
|
||||||
|
'}' => self.close_block('}'),
|
||||||
|
']' => self.close_block(']'),
|
||||||
|
';' => self.end_statement(),
|
||||||
|
',' => self.write_comma(),
|
||||||
|
'.' => self.write_compact('.'),
|
||||||
|
':' => self.write_spaced_operator(":"),
|
||||||
|
'(' => self.write_compact('('),
|
||||||
|
')' => self.write_compact(')'),
|
||||||
|
'+' | '-' | '*' | '/' | '=' | '!' | '&' | '|' | '>' | '<' => {
|
||||||
|
self.write_operator(ch)
|
||||||
|
}
|
||||||
|
_ => self.write_word(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
if !self.out.ends_with('\n') {
|
||||||
|
self.out.push('\n');
|
||||||
|
}
|
||||||
|
self.out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_whitespace(&mut self, ch: char) {
|
||||||
|
if ch == '\n' && self.out.ends_with('\n') && !self.out.ends_with("\n\n") {
|
||||||
|
self.out.push('\n');
|
||||||
|
self.line_start = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_comment(&mut self) {
|
||||||
|
if self.line_start {
|
||||||
|
self.write_indent();
|
||||||
|
} else {
|
||||||
|
self.ensure_space();
|
||||||
|
}
|
||||||
|
self.out.push('#');
|
||||||
|
for ch in self.chars.by_ref() {
|
||||||
|
if ch == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.out.push(ch);
|
||||||
|
}
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.out.push('\n');
|
||||||
|
self.line_start = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_string(&mut self) {
|
||||||
|
self.write_indent_if_needed();
|
||||||
|
self.ensure_word_boundary();
|
||||||
|
self.out.push('"');
|
||||||
|
let mut escaped = false;
|
||||||
|
for ch in self.chars.by_ref() {
|
||||||
|
self.out.push(ch);
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if ch == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if ch == '"' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_block(&mut self, ch: char) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.out.push(' ');
|
||||||
|
self.out.push(ch);
|
||||||
|
self.out.push('\n');
|
||||||
|
self.indent += INDENT;
|
||||||
|
self.line_start = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_block(&mut self, ch: char) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
if !self.out.ends_with('\n') {
|
||||||
|
self.out.push('\n');
|
||||||
|
}
|
||||||
|
self.indent = self.indent.saturating_sub(INDENT);
|
||||||
|
self.write_indent();
|
||||||
|
self.out.push(ch);
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_statement(&mut self) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.out.push(';');
|
||||||
|
self.out.push('\n');
|
||||||
|
self.line_start = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_comma(&mut self) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.out.push(',');
|
||||||
|
self.out.push(' ');
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_operator(&mut self, ch: char) {
|
||||||
|
let mut op = String::new();
|
||||||
|
op.push(ch);
|
||||||
|
if let Some(next) = self.chars.peek().copied() {
|
||||||
|
let two_char = matches!(
|
||||||
|
(ch, next),
|
||||||
|
('=', '=')
|
||||||
|
| ('!', '=')
|
||||||
|
| ('>', '=')
|
||||||
|
| ('<', '=')
|
||||||
|
| ('+', '+')
|
||||||
|
| ('/', '/')
|
||||||
|
| ('&', '&')
|
||||||
|
| ('|', '|')
|
||||||
|
| ('=', '>')
|
||||||
|
);
|
||||||
|
if two_char {
|
||||||
|
op.push(next);
|
||||||
|
self.chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.write_spaced_operator(&op);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_spaced_operator(&mut self, op: &str) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.ensure_space();
|
||||||
|
self.out.push_str(op);
|
||||||
|
self.out.push(' ');
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_compact(&mut self, ch: char) {
|
||||||
|
self.trim_trailing_spaces();
|
||||||
|
self.write_indent_if_needed();
|
||||||
|
self.out.push(ch);
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_word(&mut self, first: char) {
|
||||||
|
self.write_indent_if_needed();
|
||||||
|
self.ensure_word_boundary();
|
||||||
|
self.out.push(first);
|
||||||
|
while let Some(next) = self.chars.peek().copied() {
|
||||||
|
if next.is_alphanumeric() || next == '_' {
|
||||||
|
self.out.push(next);
|
||||||
|
self.chars.next();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_indent_if_needed(&mut self) {
|
||||||
|
if self.line_start {
|
||||||
|
self.write_indent();
|
||||||
|
self.line_start = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_indent(&mut self) {
|
||||||
|
for _ in 0..self.indent {
|
||||||
|
self.out.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_word_boundary(&mut self) {
|
||||||
|
let needs_space = self
|
||||||
|
.out
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.is_some_and(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '"'));
|
||||||
|
if needs_space {
|
||||||
|
self.out.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_space(&mut self) {
|
||||||
|
let needs_space = self
|
||||||
|
.out
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.is_some_and(|ch| !matches!(ch, ' ' | '\n' | '.' | '(' | '['));
|
||||||
|
if needs_space {
|
||||||
|
self.out.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_trailing_spaces(&mut self) {
|
||||||
|
while self.out.ends_with(' ') || self.out.ends_with('\t') {
|
||||||
|
self.out.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
struct Formatter<'a> {
|
||||||
|
source: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl<'a> Formatter<'a> {
|
||||||
|
fn new(source: &'a str) -> Self {
|
||||||
|
Self { source }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_source_file(&mut self, node: Node<'a>) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let children = named_children(node);
|
||||||
|
self.write_statement_list(&mut out, &children, 0);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_statement_list(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
|
||||||
|
let mut pending_comments = Vec::new();
|
||||||
|
let mut pending_blank = false;
|
||||||
|
let mut previous_end = 0;
|
||||||
|
let mut previous_statement_end_row = None;
|
||||||
|
|
||||||
|
for &child in children {
|
||||||
|
let is_trailing_comment = child.kind() == "comment"
|
||||||
|
&& previous_statement_end_row == Some(child.start_position().row);
|
||||||
|
if previous_statement_end_row.is_some() && !is_trailing_comment {
|
||||||
|
if !out.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
previous_statement_end_row = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous_end > 0 && has_blank_line(self.slice(previous_end, child.start_byte())) {
|
||||||
|
pending_blank = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.kind() == "comment" {
|
||||||
|
if is_trailing_comment {
|
||||||
|
if !out.ends_with(' ') {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
out.push_str(self.raw_trimmed(child));
|
||||||
|
out.push('\n');
|
||||||
|
previous_statement_end_row = None;
|
||||||
|
} else {
|
||||||
|
pending_comments.push(child);
|
||||||
|
}
|
||||||
|
previous_end = child.end_byte();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
pending_blank = false;
|
||||||
|
|
||||||
|
for comment in pending_comments.drain(..) {
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
write_indent(out, indent);
|
||||||
|
self.write_statement(out, child, indent);
|
||||||
|
previous_statement_end_row = Some(child.end_position().row);
|
||||||
|
previous_end = child.end_byte();
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous_statement_end_row.is_some() && !out.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
for comment in pending_comments {
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_statement(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
match node.kind() {
|
||||||
|
"field_definition" => {
|
||||||
|
self.write_field_definition(out, node, indent);
|
||||||
|
out.push(';');
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.write_expr(out, node, indent, 0);
|
||||||
|
out.push(';');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_field_definition(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
if let Some(path) = node.child_by_field_name("path") {
|
||||||
|
self.write_field_path(out, path);
|
||||||
|
} else {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.push_str(" = ");
|
||||||
|
if let Some(value) = node.child_by_field_name("value") {
|
||||||
|
self.write_expr(out, value, indent, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_field_path(&mut self, out: &mut String, node: Node<'a>) {
|
||||||
|
let identifiers: Vec<_> = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| child.kind() == "identifier")
|
||||||
|
.collect();
|
||||||
|
if identifiers.is_empty() {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (index, identifier) in identifiers.into_iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push('.');
|
||||||
|
}
|
||||||
|
out.push_str(self.raw_trimmed(identifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_expr(&mut self, out: &mut String, node: Node<'a>, indent: usize, parent_prec: u8) {
|
||||||
|
if expression_contains_comment(node)
|
||||||
|
&& !matches!(
|
||||||
|
node.kind(),
|
||||||
|
"object" | "array" | "let_expression" | "match_expression"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prec = self.precedence(node);
|
||||||
|
let parenthesize = prec < parent_prec;
|
||||||
|
if parenthesize {
|
||||||
|
out.push('(');
|
||||||
|
}
|
||||||
|
|
||||||
|
match node.kind() {
|
||||||
|
"literal" => {
|
||||||
|
if let Some(child) = named_children(node).first().copied() {
|
||||||
|
out.push_str(self.raw_trimmed(child));
|
||||||
|
} else {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"identifier" | "string" | "integer" | "float" | "boolean" | "regex_literal" => {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
}
|
||||||
|
"comparison_constraint" => self.write_comparison_constraint(out, node, indent),
|
||||||
|
"object" => self.write_object(out, node, indent),
|
||||||
|
"array" => self.write_array(out, node, indent),
|
||||||
|
"let_expression" => self.write_let(out, node, indent),
|
||||||
|
"function_expression" => self.write_function(out, node, indent),
|
||||||
|
"match_expression" => self.write_match(out, node, indent),
|
||||||
|
"import_expression" => self.write_import(out, node),
|
||||||
|
"parenthesized_expression" => {
|
||||||
|
out.push('(');
|
||||||
|
if let Some(expr) = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.find(|child| child.kind() != "comment")
|
||||||
|
{
|
||||||
|
self.write_expr(out, expr, indent, 0);
|
||||||
|
}
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
"call_expression" => self.write_call(out, node, indent),
|
||||||
|
"path_expression" => self.write_path(out, node, indent),
|
||||||
|
"unary_expression" => self.write_unary(out, node, indent),
|
||||||
|
"binary_expression" | "default_expression" => {
|
||||||
|
self.write_binary_like(out, node, indent, prec)
|
||||||
|
}
|
||||||
|
_ => out.push_str(self.raw_trimmed(node)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if parenthesize {
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_object(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
let children = named_children(node);
|
||||||
|
if children.is_empty() {
|
||||||
|
out.push_str("{}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.push('{');
|
||||||
|
out.push('\n');
|
||||||
|
self.write_statement_list(out, &children, indent + INDENT);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_array(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
let children = named_children(node);
|
||||||
|
if children.is_empty() {
|
||||||
|
out.push_str("[]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let has_comment = children.iter().any(|child| child.kind() == "comment");
|
||||||
|
let inline = !has_comment && children.iter().all(|child| is_inline_expr(*child));
|
||||||
|
if inline {
|
||||||
|
out.push('[');
|
||||||
|
for (index, child) in children.into_iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
self.write_expr(out, child, indent, 0);
|
||||||
|
}
|
||||||
|
out.push(']');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('[');
|
||||||
|
out.push('\n');
|
||||||
|
let mut pending_comments = Vec::new();
|
||||||
|
for child in children {
|
||||||
|
if child.kind() == "comment" {
|
||||||
|
pending_comments.push(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for comment in pending_comments.drain(..) {
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
self.write_expr(out, child, indent + INDENT, 0);
|
||||||
|
out.push_str(",\n");
|
||||||
|
}
|
||||||
|
for comment in pending_comments {
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_let(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
let body = node.child_by_field_name("body");
|
||||||
|
out.push_str("let");
|
||||||
|
out.push('\n');
|
||||||
|
let children: Vec<_> = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| Some(*child) != body)
|
||||||
|
.collect();
|
||||||
|
self.write_statement_list(out, &children, indent + INDENT);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str("in");
|
||||||
|
if let Some(body) = body {
|
||||||
|
out.push('\n');
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
self.write_expr(out, body, indent + INDENT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_function(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
let body = node.child_by_field_name("body");
|
||||||
|
let params: Vec<_> = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| child.kind() == "parameter")
|
||||||
|
.collect();
|
||||||
|
out.push('(');
|
||||||
|
for (index, param) in params.into_iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
self.write_parameter(out, param, indent);
|
||||||
|
}
|
||||||
|
out.push_str(") =>");
|
||||||
|
if let Some(body) = body {
|
||||||
|
if is_inline_expr(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_parameter(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
if let Some(name) = node.child_by_field_name("name") {
|
||||||
|
out.push_str(self.raw_trimmed(name));
|
||||||
|
}
|
||||||
|
if let Some(constraint) = node.child_by_field_name("constraint") {
|
||||||
|
out.push_str(": ");
|
||||||
|
self.write_expr(out, constraint, indent, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_match(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
out.push_str("match ");
|
||||||
|
if let Some(scrutinee) = node.child_by_field_name("scrutinee") {
|
||||||
|
self.write_expr(out, scrutinee, indent, 0);
|
||||||
|
}
|
||||||
|
out.push_str(" {");
|
||||||
|
let children: Vec<_> = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| child.kind() == "match_arm" || child.kind() == "comment")
|
||||||
|
.collect();
|
||||||
|
if !children.is_empty() {
|
||||||
|
out.push('\n');
|
||||||
|
self.write_match_arms(out, &children, indent + INDENT);
|
||||||
|
write_indent(out, indent);
|
||||||
|
}
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_match_arms(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
|
||||||
|
let mut pending_comments = Vec::new();
|
||||||
|
for &child in children {
|
||||||
|
if child.kind() == "comment" {
|
||||||
|
pending_comments.push(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for comment in pending_comments.drain(..) {
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
write_indent(out, indent);
|
||||||
|
if let Some(pattern) = child.child_by_field_name("pattern") {
|
||||||
|
self.write_expr(out, pattern, indent, 0);
|
||||||
|
}
|
||||||
|
out.push_str(": ");
|
||||||
|
if let Some(body) = child.child_by_field_name("body") {
|
||||||
|
self.write_expr(out, body, indent, 0);
|
||||||
|
}
|
||||||
|
out.push_str(";\n");
|
||||||
|
}
|
||||||
|
for comment in pending_comments {
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str(self.raw_trimmed(comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_import(&mut self, out: &mut String, node: Node<'a>) {
|
||||||
|
out.push_str("import ");
|
||||||
|
if let Some(specifier) = node.child_by_field_name("specifier") {
|
||||||
|
out.push_str(self.raw_trimmed(specifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_call(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
let callee = node.child_by_field_name("function");
|
||||||
|
if let Some(callee) = callee {
|
||||||
|
self.write_expr(out, callee, indent, self.precedence(node));
|
||||||
|
}
|
||||||
|
out.push('(');
|
||||||
|
let args: Vec<_> = named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|child| Some(*child) != callee && child.kind() != "comment")
|
||||||
|
.collect();
|
||||||
|
for (index, arg) in args.into_iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
self.write_expr(out, arg, indent, 0);
|
||||||
|
}
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_path(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
if let Some(object) = node.child_by_field_name("object") {
|
||||||
|
self.write_expr(out, object, indent, self.precedence(node));
|
||||||
|
}
|
||||||
|
out.push('.');
|
||||||
|
if let Some(field) = node.child_by_field_name("field") {
|
||||||
|
out.push_str(self.raw_trimmed(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_unary(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
if let Some(operand) = node.child_by_field_name("operand") {
|
||||||
|
let operator = self.slice(node.start_byte(), operand.start_byte()).trim();
|
||||||
|
out.push_str(operator);
|
||||||
|
self.write_expr(out, operand, indent, self.precedence(node));
|
||||||
|
} else {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_comparison_constraint(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
||||||
|
if let Some(value) = node.child_by_field_name("value") {
|
||||||
|
let operator = self.slice(node.start_byte(), value.start_byte()).trim();
|
||||||
|
out.push_str(operator);
|
||||||
|
out.push(' ');
|
||||||
|
self.write_expr(out, value, indent, self.precedence(node));
|
||||||
|
} else {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_binary_like(&mut self, out: &mut String, node: Node<'a>, indent: usize, prec: u8) {
|
||||||
|
let left = node
|
||||||
|
.child_by_field_name("left")
|
||||||
|
.or_else(|| node.child_by_field_name("base"));
|
||||||
|
let right = node
|
||||||
|
.child_by_field_name("right")
|
||||||
|
.or_else(|| node.child_by_field_name("fallback"));
|
||||||
|
let (Some(left), Some(right)) = (left, right) else {
|
||||||
|
out.push_str(self.raw_trimmed(node));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.write_expr(out, left, indent, prec);
|
||||||
|
out.push(' ');
|
||||||
|
if node.kind() == "default_expression" {
|
||||||
|
out.push_str("default");
|
||||||
|
} else {
|
||||||
|
out.push_str(self.slice(left.end_byte(), right.start_byte()).trim());
|
||||||
|
}
|
||||||
|
out.push(' ');
|
||||||
|
self.write_expr(out, right, indent, prec + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn precedence(&self, node: Node<'a>) -> u8 {
|
||||||
|
match node.kind() {
|
||||||
|
"default_expression" => 1,
|
||||||
|
"binary_expression" => self.binary_precedence(node),
|
||||||
|
"unary_expression" | "comparison_constraint" => 17,
|
||||||
|
"call_expression" | "path_expression" => 19,
|
||||||
|
_ => 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_precedence(&self, node: Node<'a>) -> u8 {
|
||||||
|
let Some(left) = node.child_by_field_name("left") else {
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
let Some(right) = node.child_by_field_name("right") else {
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
match self.slice(left.end_byte(), right.start_byte()).trim() {
|
||||||
|
"//" => 3,
|
||||||
|
"&" => 5,
|
||||||
|
"||" => 7,
|
||||||
|
"&&" => 9,
|
||||||
|
"==" | "!=" | ">" | ">=" | "<" | "<=" => 11,
|
||||||
|
"++" => 12,
|
||||||
|
"+" | "-" => 13,
|
||||||
|
"*" | "/" => 15,
|
||||||
|
_ => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_trimmed(&self, node: Node<'a>) -> &'a str {
|
||||||
|
self.slice(node.start_byte(), node.end_byte()).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice(&self, start: usize, end: usize) -> &'a str {
|
||||||
|
&self.source[start..end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn named_children<'a>(node: Node<'a>) -> Vec<Node<'a>> {
|
||||||
|
let mut cursor: TreeCursor<'a> = node.walk();
|
||||||
|
node.named_children(&mut cursor).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn expression_contains_comment(node: Node<'_>) -> bool {
|
||||||
|
if node.kind() == "comment" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
named_children(node)
|
||||||
|
.into_iter()
|
||||||
|
.any(expression_contains_comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn is_inline_expr(node: Node<'_>) -> bool {
|
||||||
|
match node.kind() {
|
||||||
|
"literal"
|
||||||
|
| "identifier"
|
||||||
|
| "string"
|
||||||
|
| "integer"
|
||||||
|
| "float"
|
||||||
|
| "boolean"
|
||||||
|
| "regex_literal"
|
||||||
|
| "comparison_constraint"
|
||||||
|
| "import_expression" => true,
|
||||||
|
"path_expression"
|
||||||
|
| "call_expression"
|
||||||
|
| "unary_expression"
|
||||||
|
| "binary_expression"
|
||||||
|
| "default_expression"
|
||||||
|
| "parenthesized_expression" => {
|
||||||
|
!expression_contains_comment(node)
|
||||||
|
&& named_children(node).into_iter().all(is_inline_expr)
|
||||||
|
}
|
||||||
|
"array" => {
|
||||||
|
!expression_contains_comment(node)
|
||||||
|
&& named_children(node).into_iter().all(is_inline_expr)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn has_blank_line(text: &str) -> bool {
|
||||||
|
text.bytes().filter(|byte| *byte == b'\n').count() >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn write_indent(out: &mut String, indent: usize) {
|
||||||
|
for _ in 0..indent {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_fields_and_preserves_comments() {
|
||||||
|
let source =
|
||||||
|
"Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_arrays_and_match_comments() {
|
||||||
|
let source = "result=match x{# c\n_: [1,{a=2;}];};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wasm_export_returns_json() {
|
||||||
|
let output = format_source_json("value={a=1;};");
|
||||||
|
assert!(output.contains("\"ok\":true"));
|
||||||
|
assert!(output.contains("value = {\\n a = 1;\\n};\\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,10 +39,25 @@ The JSR package is `@hare/decodal-wasm`.
|
|||||||
The generated files in `packages/decodal-wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
|
The generated files in `packages/decodal-wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
|
||||||
The WebAssembly package is for execution, not syntax highlighting.
|
The WebAssembly package is for execution, not syntax highlighting.
|
||||||
|
|
||||||
|
## Language tools
|
||||||
|
|
||||||
|
Source-level tooling lives in the Rust language tools crate.
|
||||||
|
This component is responsible for operations that must preserve source text details such as comments and whitespace.
|
||||||
|
It is an internal crate used by the future Rust LSP and by the CodeMirror package's bundled formatter WebAssembly.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-language-tools/
|
||||||
|
```
|
||||||
|
|
||||||
|
The current language tools crate exposes the formatter.
|
||||||
|
Future LSP functionality should build on the same source-level tooling layer rather than on the runtime core.
|
||||||
|
|
||||||
## Web editor components
|
## Web editor components
|
||||||
|
|
||||||
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
|
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
|
||||||
This is the source of syntax highlighting, folding, indentation, and editor syntax tree behavior in the browser UI.
|
This is the source of syntax highlighting, folding, indentation, editor syntax tree behavior, and the browser formatter command in the browser UI.
|
||||||
|
|
||||||
Important paths:
|
Important paths:
|
||||||
|
|
||||||
@@ -51,6 +66,8 @@ editors/lezer-decodal/decodal.grammar
|
|||||||
packages/decodal-codemirror/src/decodal.js
|
packages/decodal-codemirror/src/decodal.js
|
||||||
packages/decodal-codemirror/src/decodal-parser.js
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
packages/decodal-codemirror/src/decodal-parser.terms.js
|
packages/decodal-codemirror/src/decodal-parser.terms.js
|
||||||
|
packages/decodal-codemirror/src/format.js
|
||||||
|
packages/decodal-codemirror/wasm/
|
||||||
```
|
```
|
||||||
|
|
||||||
The npm package is `decodal-codemirror`.
|
The npm package is `decodal-codemirror`.
|
||||||
@@ -94,7 +111,8 @@ Consumers that need syntax information should use the component that matches the
|
|||||||
|
|
||||||
- Rust execution and embedding: `decodal`
|
- Rust execution and embedding: `decodal`
|
||||||
- Browser execution: `decodal-wasm`
|
- Browser execution: `decodal-wasm`
|
||||||
- Web editor syntax: Lezer / CodeMirror
|
- Web formatting and editor syntax: Lezer / CodeMirror
|
||||||
|
- Rust LSP internals: `decodal-language-tools`
|
||||||
- General editor syntax: Tree-sitter
|
- General editor syntax: Tree-sitter
|
||||||
|
|
||||||
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
|||||||
|
|
||||||
The primary crates.io package is `decodal`, which contains the embeddable library.
|
The primary crates.io package is `decodal`, which contains the embeddable library.
|
||||||
`decodal-derive` provides optional derive macros for Rust struct integration and is published only when the derive crate changes.
|
`decodal-derive` provides optional derive macros for Rust struct integration and is published only when the derive crate changes.
|
||||||
Workspace support crates such as `decodal-cli` and the Rust source crate `decodal-wasm` are not published to crates.io.
|
Workspace support crates such as `decodal-cli`, the Rust source crate `decodal-wasm`, and `decodal-language-tools` are not published to crates.io.
|
||||||
The generated WebAssembly package under `packages/decodal-wasm/` is published to npm and JSR.
|
The generated WebAssembly package under `packages/decodal-wasm/` is published to npm and JSR.
|
||||||
|
The CodeMirror package bundles the generated formatter WebAssembly from `decodal-language-tools`.
|
||||||
The project is dual licensed as `MIT OR Apache-2.0`.
|
The project is dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -58,7 +59,7 @@ site/decodal-site/
|
|||||||
```
|
```
|
||||||
|
|
||||||
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
|
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
|
||||||
The playground loads `decodal-wasm` and evaluates DCDL entirely in the browser.
|
The playground loads `decodal-wasm` for evaluation and `decodal-codemirror` for editor features and source formatting.
|
||||||
|
|
||||||
Important files:
|
Important files:
|
||||||
|
|
||||||
@@ -74,10 +75,13 @@ packages/decodal-codemirror/src/decodal.js
|
|||||||
packages/decodal-codemirror/src/decodal-parser.js
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
packages/decodal-wasm/decodal_wasm.js
|
packages/decodal-wasm/decodal_wasm.js
|
||||||
packages/decodal-wasm/decodal_wasm_bg.wasm
|
packages/decodal-wasm/decodal_wasm_bg.wasm
|
||||||
|
packages/decodal-codemirror/wasm/decodal_language_tools.js
|
||||||
|
packages/decodal-codemirror/wasm/decodal_language_tools_bg.wasm
|
||||||
crates/decodal-wasm/src/lib.rs
|
crates/decodal-wasm/src/lib.rs
|
||||||
|
crates/decodal-language-tools/src/lib.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the WebAssembly package before building the site:
|
Build the WebAssembly packages before building the site:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd site/decodal-site
|
cd site/decodal-site
|
||||||
@@ -86,13 +90,11 @@ npm run build:wasm
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
`npm run build:wasm` writes generated files into:
|
`npm run build:wasm` builds both generated WebAssembly packages.
|
||||||
|
`npm run build:runtime` writes generated runtime files into `packages/decodal-wasm/`.
|
||||||
|
`npm run build:tools` writes generated language tools files into `packages/decodal-codemirror/wasm/`.
|
||||||
|
|
||||||
```text
|
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
|
||||||
packages/decodal-wasm/
|
|
||||||
```
|
|
||||||
|
|
||||||
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm package first.
|
|
||||||
|
|
||||||
Publish the generated WebAssembly package from its package directory:
|
Publish the generated WebAssembly package from its package directory:
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
CodeMirror 6 language support for Decodal.
|
CodeMirror 6 language support for Decodal.
|
||||||
|
|
||||||
This package provides the Lezer parser metadata, highlighting tags, indentation rules, and block folding used by the Decodal Web playground.
|
This package provides the Lezer parser metadata, highlighting tags, indentation rules, block folding, and formatter command used by the Decodal Web playground.
|
||||||
It does not include the Decodal runtime or WebAssembly evaluator.
|
It does not include the Decodal runtime evaluator.
|
||||||
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
|
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -52,3 +52,23 @@ decodal({ highlight: false })
|
|||||||
```
|
```
|
||||||
|
|
||||||
Use this when you want the language support without the bundled highlight style.
|
Use this when you want the language support without the bundled highlight style.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
The formatter is exposed as a separate subpath so plain syntax highlighting does not force callers to initialize WebAssembly.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
formatDecodal,
|
||||||
|
formatDecodalCommand,
|
||||||
|
initDecodalFormatter,
|
||||||
|
} from 'decodal-codemirror/format';
|
||||||
|
import formatterWasm from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
|
|
||||||
|
await initDecodalFormatter(formatterWasm);
|
||||||
|
|
||||||
|
const result = formatDecodal('Server={port=Int default 8080;};');
|
||||||
|
if (result.ok) console.log(result.source);
|
||||||
|
|
||||||
|
formatDecodalCommand(view);
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@hare/decodal-codemirror",
|
"name": "@hare/decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": "./src/mod.ts",
|
"exports": {
|
||||||
|
".": "./src/mod.ts",
|
||||||
|
"./format": "./src/format.ts"
|
||||||
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
|
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
|
||||||
|
"@codemirror/view": "npm:@codemirror/view@^6.43.6",
|
||||||
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
|
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
|
||||||
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
|
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
|
||||||
},
|
},
|
||||||
@@ -12,12 +16,20 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"src/mod.ts",
|
"src/mod.ts",
|
||||||
|
"src/format.ts",
|
||||||
"src/decodal.js",
|
"src/decodal.js",
|
||||||
"src/decodal.d.ts",
|
"src/decodal.d.ts",
|
||||||
"src/decodal.js.d.ts",
|
"src/decodal.js.d.ts",
|
||||||
|
"src/format.js",
|
||||||
|
"src/format.d.ts",
|
||||||
|
"src/format.js.d.ts",
|
||||||
"src/decodal-parser-jsr.js",
|
"src/decodal-parser-jsr.js",
|
||||||
"src/decodal-parser.js",
|
"src/decodal-parser.js",
|
||||||
"src/decodal-parser.terms.js",
|
"src/decodal-parser.terms.js",
|
||||||
|
"wasm/decodal_language_tools.js",
|
||||||
|
"wasm/decodal_language_tools.d.ts",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm.d.ts",
|
||||||
"package.json"
|
"package.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -1,15 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.10"
|
"@lezer/lr": "^1.4.10"
|
||||||
}
|
}
|
||||||
@@ -18,6 +25,7 @@
|
|||||||
"version": "6.12.4",
|
"version": "6.12.4",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
@@ -32,6 +40,7 @@
|
|||||||
"version": "6.7.1",
|
"version": "6.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
@@ -41,6 +50,7 @@
|
|||||||
"version": "6.43.6",
|
"version": "6.43.6",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||||
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.7.0",
|
"@codemirror/state": "^6.7.0",
|
||||||
@@ -53,12 +63,14 @@
|
|||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@lezer/highlight": {
|
"node_modules/@lezer/highlight": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.3.0"
|
"@lezer/common": "^1.3.0"
|
||||||
@@ -68,6 +80,7 @@
|
|||||||
"version": "1.4.10",
|
"version": "1.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.0.0"
|
"@lezer/common": "^1.0.0"
|
||||||
@@ -77,24 +90,28 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/crelt": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/style-mod": {
|
"node_modules/style-mod": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/w3c-keyname": {
|
"node_modules/w3c-keyname": {
|
||||||
"version": "2.2.8",
|
"version": "2.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"description": "CodeMirror 6 language support for Decodal.",
|
"description": "CodeMirror 6 language support for Decodal.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
@@ -14,21 +14,37 @@
|
|||||||
"types": "./src/decodal.d.ts",
|
"types": "./src/decodal.d.ts",
|
||||||
"import": "./src/decodal.js"
|
"import": "./src/decodal.js"
|
||||||
},
|
},
|
||||||
|
"./format": {
|
||||||
|
"types": "./src/format.d.ts",
|
||||||
|
"import": "./src/format.js"
|
||||||
|
},
|
||||||
"./parser": "./src/decodal-parser.js",
|
"./parser": "./src/decodal-parser.js",
|
||||||
"./terms": "./src/decodal-parser.terms.js"
|
"./terms": "./src/decodal-parser.terms.js",
|
||||||
|
"./wasm/decodal_language_tools_bg.wasm": "./wasm/decodal_language_tools_bg.wasm"
|
||||||
},
|
},
|
||||||
"types": "./src/decodal.d.ts",
|
"types": "./src/decodal.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"src/"
|
"src/",
|
||||||
|
"wasm/decodal_language_tools.js",
|
||||||
|
"wasm/decodal_language_tools.d.ts",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm.d.ts"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"decodal",
|
"decodal",
|
||||||
"codemirror",
|
"codemirror",
|
||||||
"lezer"
|
"lezer"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"peerDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.10"
|
"@lezer/lr": "^1.4.10"
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export interface FormatSuccess {
|
||||||
|
ok: true;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatFailure {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
|
||||||
|
export declare function formatDecodal(source: string): FormatResult;
|
||||||
|
export declare function formatDecodalCommand(view: EditorView): boolean;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export async function initDecodalFormatter(moduleOrPath) {
|
||||||
|
await initLanguageTools(moduleOrPath);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDecodal(source) {
|
||||||
|
if (!initialized) {
|
||||||
|
return { ok: false, error: 'Decodal formatter is not initialized' };
|
||||||
|
}
|
||||||
|
return JSON.parse(formatSource(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDecodalCommand(view) {
|
||||||
|
const result = formatDecodal(view.state.doc.toString());
|
||||||
|
if (!result.ok) return false;
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: result.source,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export interface FormatSuccess {
|
||||||
|
ok: true;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatFailure {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
|
||||||
|
export declare function formatDecodal(source: string): FormatResult;
|
||||||
|
export declare function formatDecodalCommand(view: EditorView): boolean;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Decodal source formatting helpers for CodeMirror integrations.
|
||||||
|
*
|
||||||
|
* The formatter is backed by WebAssembly generated from the internal Rust
|
||||||
|
* `decodal-language-tools` crate. Call {@link initDecodalFormatter} once before
|
||||||
|
* using {@link formatDecodal} or {@link formatDecodalCommand}.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import type { EditorView } from 'npm:@codemirror/view@^6.43.6';
|
||||||
|
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
/** Successful formatter result. */
|
||||||
|
export interface FormatSuccess {
|
||||||
|
/** Indicates that formatting succeeded. */
|
||||||
|
ok: true;
|
||||||
|
/** The formatted Decodal source text. */
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Failed formatter result. */
|
||||||
|
export interface FormatFailure {
|
||||||
|
/** Indicates that formatting failed. */
|
||||||
|
ok: false;
|
||||||
|
/** Human-readable formatter error. */
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result returned by {@link formatDecodal}. */
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the bundled Decodal formatter WebAssembly module.
|
||||||
|
*
|
||||||
|
* Bundlers such as Vite should pass the package wasm asset URL explicitly:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import wasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
|
* await initDecodalFormatter(wasmUrl);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param moduleOrPath - Optional WebAssembly module, bytes, response, URL, or
|
||||||
|
* request accepted by wasm-bindgen's generated initializer.
|
||||||
|
*/
|
||||||
|
export async function initDecodalFormatter(
|
||||||
|
moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module,
|
||||||
|
): Promise<void> {
|
||||||
|
await initLanguageTools(moduleOrPath);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a complete Decodal source document.
|
||||||
|
*
|
||||||
|
* @param source - Source text to format.
|
||||||
|
* @returns Either formatted source text or an error message.
|
||||||
|
*/
|
||||||
|
export function formatDecodal(source: string): FormatResult {
|
||||||
|
if (!initialized) {
|
||||||
|
return { ok: false, error: 'Decodal formatter is not initialized' };
|
||||||
|
}
|
||||||
|
return JSON.parse(formatSource(source)) as FormatResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodeMirror command that formats the whole document in-place.
|
||||||
|
*
|
||||||
|
* @param view - The editor view whose document should be formatted.
|
||||||
|
* @returns `true` when the document was formatted, otherwise `false`.
|
||||||
|
*/
|
||||||
|
export function formatDecodalCommand(view: EditorView): boolean {
|
||||||
|
const result = formatDecodal(view.state.doc.toString());
|
||||||
|
if (!result.ok) return false;
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: result.source,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* CodeMirror 6 language support for Decodal.
|
||||||
|
*
|
||||||
|
* This module exports the Decodal language extension, parser metadata, and the
|
||||||
|
* bundled highlight style used by the official Decodal playground. Add
|
||||||
|
* {@link decodal} to a CodeMirror extension set to enable highlighting,
|
||||||
|
* indentation, and folding for Decodal documents.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
||||||
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
|
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
|
||||||
import { parser } from './decodal-parser-jsr.js';
|
import { parser } from './decodal-parser-jsr.js';
|
||||||
@@ -44,6 +54,7 @@ function foldDelimited(open: string, close: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Decodal LR language definition for CodeMirror 6. */
|
||||||
export const decodalLanguage: LRLanguage = LRLanguage.define({
|
export const decodalLanguage: LRLanguage = LRLanguage.define({
|
||||||
parser: parserWithMetadata,
|
parser: parserWithMetadata,
|
||||||
languageData: {
|
languageData: {
|
||||||
@@ -52,6 +63,7 @@ export const decodalLanguage: LRLanguage = LRLanguage.define({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Default Decodal highlight style used by the playground. */
|
||||||
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
||||||
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
||||||
{ tag: t.variableName, color: 'inherit' },
|
{ tag: t.variableName, color: 'inherit' },
|
||||||
@@ -64,10 +76,19 @@ export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
|||||||
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** Options for {@link decodal}. */
|
||||||
export interface DecodalOptions {
|
export interface DecodalOptions {
|
||||||
|
/** Include the bundled {@link decodalHighlightStyle}. Defaults to `true`. */
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create CodeMirror language support for Decodal.
|
||||||
|
*
|
||||||
|
* @param options - Optional language support options.
|
||||||
|
* @returns A CodeMirror extension bundle containing the Decodal language and,
|
||||||
|
* unless disabled, the default highlight style.
|
||||||
|
*/
|
||||||
export function decodal(options: DecodalOptions = {}): LanguageSupport {
|
export function decodal(options: DecodalOptions = {}): LanguageSupport {
|
||||||
const { highlight = true } = options;
|
const { highlight = true } = options;
|
||||||
return new LanguageSupport(
|
return new LanguageSupport(
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# wasm-pack output is committed as part of decodal-codemirror.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export function formatSource(source: string): string;
|
||||||
|
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
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;
|
||||||
|
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wbindgen_start: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the given `module`, which can either be bytes or
|
||||||
|
* a precompiled `WebAssembly.Module`.
|
||||||
|
*
|
||||||
|
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {InitOutput}
|
||||||
|
*/
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/* @ts-self-types="./decodal_language_tools.d.ts" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,
|
||||||
|
__wbindgen_init_externref_table: function() {
|
||||||
|
const table = wasm.__wbindgen_externrefs;
|
||||||
|
const offset = table.grow(4);
|
||||||
|
table.set(0, undefined);
|
||||||
|
table.set(offset + 0, undefined);
|
||||||
|
table.set(offset + 1, null);
|
||||||
|
table.set(offset + 2, true);
|
||||||
|
table.set(offset + 3, false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
__proto__: null,
|
||||||
|
"./decodal_language_tools_bg.js": import0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
return decodeText(ptr >>> 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||||
|
let numBytesDecoded = 0;
|
||||||
|
function decodeText(ptr, len) {
|
||||||
|
numBytesDecoded += len;
|
||||||
|
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||||
|
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
numBytesDecoded = len;
|
||||||
|
}
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextEncoder = new TextEncoder();
|
||||||
|
|
||||||
|
if (!('encodeInto' in cachedTextEncoder)) {
|
||||||
|
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
let wasmModule, wasmInstance, wasm;
|
||||||
|
function __wbg_finalize_init(instance, module) {
|
||||||
|
wasmInstance = instance;
|
||||||
|
wasm = instance.exports;
|
||||||
|
wasmModule = module;
|
||||||
|
cachedUint8ArrayMemory0 = null;
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
} catch (e) {
|
||||||
|
const validResponse = module.ok && expectedResponseType(module.type);
|
||||||
|
|
||||||
|
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else { throw e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectedResponseType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'basic': case 'cors': case 'default': return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSync(module) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (module !== undefined) {
|
||||||
|
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||||
|
({module} = module)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
|
module = new WebAssembly.Module(module);
|
||||||
|
}
|
||||||
|
const instance = new WebAssembly.Instance(module, imports);
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_init(module_or_path) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (module_or_path !== undefined) {
|
||||||
|
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||||
|
({module_or_path} = module_or_path)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module_or_path === undefined) {
|
||||||
|
module_or_path = new URL('decodal_language_tools_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||||
|
module_or_path = fetch(module_or_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSync, __wbg_init as default };
|
||||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
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;
|
||||||
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wbindgen_start: () => void;
|
||||||
@@ -26,11 +26,8 @@ import init, { evaluateProject } from 'decodal-wasm';
|
|||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const result = evaluateProject(JSON.stringify({
|
const result = evaluateProject('main.dcdl', JSON.stringify({
|
||||||
entry: 'main.dcdl',
|
|
||||||
files: {
|
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@hare/decodal-wasm",
|
"name": "@hare/decodal-wasm",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": "./decodal_wasm.js",
|
"exports": "./mod.ts",
|
||||||
"publish": {
|
"publish": {
|
||||||
"include": [
|
"include": [
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"mod.ts",
|
||||||
"decodal_wasm.js",
|
"decodal_wasm.js",
|
||||||
"decodal_wasm.d.ts",
|
"decodal_wasm.d.ts",
|
||||||
"decodal_wasm_bg.wasm",
|
"decodal_wasm_bg.wasm",
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Browser and JavaScript runtime bindings for the Decodal evaluator.
|
||||||
|
*
|
||||||
|
* This module wraps the wasm-bindgen output generated from the Rust
|
||||||
|
* `decodal-wasm` crate and exposes stable, documented entrypoints for JSR
|
||||||
|
* users. The exported evaluator functions return JSON strings.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import initWasm, {
|
||||||
|
evaluate as evaluateImpl,
|
||||||
|
evaluateProject as evaluateProjectImpl,
|
||||||
|
initSync as initSyncImpl,
|
||||||
|
} from './decodal_wasm.js';
|
||||||
|
|
||||||
|
/** Input accepted by the wasm-bindgen initializer. */
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
/** Initialized Decodal WebAssembly exports. */
|
||||||
|
export interface InitOutput {
|
||||||
|
/** Linear memory exported by the WebAssembly module. */
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly evaluate: (source: number, len: number) => [number, number, number, number];
|
||||||
|
readonly evaluateProject: (entry: number, entryLen: number, files: number, filesLen: number) => [number, number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Decodal WebAssembly runtime asynchronously.
|
||||||
|
*
|
||||||
|
* @param moduleOrPath - Optional WebAssembly module, bytes, response, URL, or request.
|
||||||
|
* @returns The initialized WebAssembly exports.
|
||||||
|
*/
|
||||||
|
export default async function initDecodalRuntime(moduleOrPath?: InitInput): Promise<InitOutput> {
|
||||||
|
return await initWasm(moduleOrPath) as InitOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Decodal WebAssembly runtime synchronously.
|
||||||
|
*
|
||||||
|
* @param module - Compiled module, bytes, or wasm-bindgen sync initialization input.
|
||||||
|
* @returns The initialized WebAssembly exports.
|
||||||
|
*/
|
||||||
|
export function initSync(module: WebAssembly.Module | BufferSource): InitOutput {
|
||||||
|
return initSyncImpl(module) as InitOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate and materialize a single Decodal source string.
|
||||||
|
*
|
||||||
|
* @param source - Decodal source text.
|
||||||
|
* @returns A JSON string containing either `{ ok: true, output }` or `{ ok: false, error }`.
|
||||||
|
*/
|
||||||
|
export function evaluate(source: string): string {
|
||||||
|
return evaluateImpl(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate and materialize a virtual multi-file Decodal project.
|
||||||
|
*
|
||||||
|
* @param entry - Entry point file path to materialize.
|
||||||
|
* @param filesJson - JSON object mapping virtual file paths to source strings.
|
||||||
|
* @returns A JSON string containing either `{ ok: true, output }` or `{ ok: false, error }`.
|
||||||
|
*/
|
||||||
|
export function evaluateProject(entry: string, filesJson: string): string {
|
||||||
|
return evaluateProjectImpl(entry, filesJson);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "decodal-wasm",
|
"name": "decodal-wasm",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
|
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -2,8 +2,18 @@ import { defineConfig } from 'astro/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'static',
|
output: 'static',
|
||||||
|
build: {
|
||||||
|
assets: 'assets',
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
resolve: {
|
resolve: {
|
||||||
|
dedupe: [
|
||||||
|
'@codemirror/language',
|
||||||
|
'@codemirror/state',
|
||||||
|
'@codemirror/view',
|
||||||
|
'@lezer/highlight',
|
||||||
|
'@lezer/lr',
|
||||||
|
],
|
||||||
preserveSymlinks: true,
|
preserveSymlinks: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+10
-3
@@ -25,16 +25,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../packages/decodal-codemirror": {
|
"../../packages/decodal-codemirror": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.10"
|
"@lezer/lr": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../packages/decodal-wasm": {
|
"../../packages/decodal-wasm": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"license": "MIT OR Apache-2.0"
|
"license": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/check": {
|
"node_modules/@astrojs/check": {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
"dev": "astro dev --host 0.0.0.0",
|
"dev": "astro dev --host 0.0.0.0",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview --host 0.0.0.0",
|
"preview": "astro preview --host 0.0.0.0",
|
||||||
"build:wasm": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../packages/decodal-wasm --release && node scripts/prepare-wasm-package.mjs",
|
"build:runtime": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../packages/decodal-wasm --release && node scripts/prepare-wasm-package.mjs",
|
||||||
|
"build:tools": "wasm-pack build ../../crates/decodal-language-tools --target web --out-dir ../../packages/decodal-codemirror/wasm --release && node scripts/prepare-codemirror-wasm.mjs",
|
||||||
|
"build:wasm": "npm run build:runtime && npm run build:tools",
|
||||||
|
"build:packages": "npm run build:wasm",
|
||||||
"deploy": "npm run build && node scripts/deploy-pages.mjs",
|
"deploy": "npm run build && node scripts/deploy-pages.mjs",
|
||||||
"deploy:wasm": "npm run build:wasm && npm run deploy"
|
"deploy:wasm": "npm run build:wasm && npm run deploy"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const wasmDir = resolve(import.meta.dirname, '../../../packages/decodal-codemirror/wasm');
|
||||||
|
|
||||||
|
rmSync(resolve(wasmDir, 'README.md'), { force: true });
|
||||||
|
rmSync(resolve(wasmDir, 'package.json'), { force: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(wasmDir, '.gitignore'),
|
||||||
|
'# wasm-pack output is committed as part of decodal-codemirror.\n',
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { writeFileSync } from 'node:fs';
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
const packageDir = resolve(import.meta.dirname, '../../../packages/decodal-wasm');
|
const packageDir = resolve(import.meta.dirname, '../../../packages/decodal-wasm');
|
||||||
@@ -8,6 +8,11 @@ writeFileSync(
|
|||||||
'# wasm-pack output is committed for the browser runtime package.\n',
|
'# wasm-pack output is committed for the browser runtime package.\n',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const packageJsonPath = resolve(packageDir, 'package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
packageJson.version = '0.1.3';
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(packageDir, 'README.md'),
|
resolve(packageDir, 'README.md'),
|
||||||
`# decodal-wasm
|
`# decodal-wasm
|
||||||
@@ -38,11 +43,8 @@ import init, { evaluateProject } from 'decodal-wasm';
|
|||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const result = evaluateProject(JSON.stringify({
|
const result = evaluateProject('main.dcdl', JSON.stringify({
|
||||||
entry: 'main.dcdl',
|
|
||||||
files: {
|
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
@@ -51,3 +53,27 @@ console.log(JSON.parse(result));
|
|||||||
The exported functions return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
The exported functions return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(packageDir, 'jsr.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: '@hare/decodal-wasm',
|
||||||
|
version: '0.1.3',
|
||||||
|
license: 'MIT',
|
||||||
|
exports: './mod.ts',
|
||||||
|
publish: {
|
||||||
|
include: [
|
||||||
|
'README.md',
|
||||||
|
'mod.ts',
|
||||||
|
'decodal_wasm.js',
|
||||||
|
'decodal_wasm.d.ts',
|
||||||
|
'decodal_wasm_bg.wasm',
|
||||||
|
'package.json',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) + '\n',
|
||||||
|
);
|
||||||
|
|||||||
@@ -27,28 +27,14 @@ Production = Server & {
|
|||||||
|
|
||||||
<section class="home-packages" aria-label="Official packages">
|
<section class="home-packages" aria-label="Official packages">
|
||||||
<h2>Official packages</h2>
|
<h2>Official packages</h2>
|
||||||
<div class="package-links">
|
<a class="primary-package" href="https://crates.io/crates/decodal">
|
||||||
<a href="https://crates.io/crates/decodal">
|
|
||||||
<span>Rust crate</span>
|
<span>Rust crate</span>
|
||||||
<strong>crates.io/crates/decodal</strong>
|
<strong>crates.io/crates/decodal</strong>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/decodal-wasm">
|
<ul class="package-list">
|
||||||
<span>npm package</span>
|
<li>decodal-wasm on <a href="https://jsr.io/@hare/decodal-wasm@0.1.3">jsr</a> / <a href="https://www.npmjs.com/package/decodal-wasm">npm</a></li>
|
||||||
<strong>npmjs.com/package/decodal-wasm</strong>
|
<li>decodal-codemirror on <a href="https://jsr.io/@hare/decodal-codemirror@0.1.5">jsr</a> / <a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></li>
|
||||||
</a>
|
</ul>
|
||||||
<a href="https://jsr.io/@hare/decodal-wasm@0.1.2">
|
|
||||||
<span>JSR package</span>
|
|
||||||
<strong>jsr.io/@hare/decodal-wasm</strong>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.npmjs.com/package/decodal-codemirror">
|
|
||||||
<span>CodeMirror package</span>
|
|
||||||
<strong>npmjs.com/package/decodal-codemirror</strong>
|
|
||||||
</a>
|
|
||||||
<a href="https://jsr.io/@hare/decodal-codemirror@0.1.2">
|
|
||||||
<span>CodeMirror on JSR</span>
|
|
||||||
<strong>jsr.io/@hare/decodal-codemirror</strong>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="home-grid" aria-label="Decodal overview">
|
<section class="home-grid" aria-label="Decodal overview">
|
||||||
@@ -62,8 +48,8 @@ Production = Server & {
|
|||||||
<article class="home-card">
|
<article class="home-card">
|
||||||
<h2>Where it runs</h2>
|
<h2>Where it runs</h2>
|
||||||
<p>
|
<p>
|
||||||
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for the Web editor,
|
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for Web editing
|
||||||
and Tree-sitter for general editor integration.
|
and formatting, and Tree-sitter for general editor integration.
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="home-card">
|
<article class="home-card">
|
||||||
|
|||||||
@@ -6,17 +6,26 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||||||
<div class="playground-header">
|
<div class="playground-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Playground</h1>
|
<h1>Playground</h1>
|
||||||
<p>Virtual files are evaluated in the browser through WebAssembly. Use import paths such as <code>./schemas/service.dcdl</code>.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="playground-actions">
|
<div class="playground-actions">
|
||||||
|
<div class="action-group example-actions">
|
||||||
<label class="example-picker">
|
<label class="example-picker">
|
||||||
<span>Example</span>
|
<span>Example</span>
|
||||||
<select id="example-select" aria-label="Playground example"></select>
|
<select id="example-select" aria-label="Playground example"></select>
|
||||||
</label>
|
</label>
|
||||||
<button id="load-example" type="button">Load</button>
|
<button id="load-example" type="button">Load</button>
|
||||||
<p id="status" class="status">Loading WASM...</p>
|
</div>
|
||||||
|
<span class="action-separator" aria-hidden="true">|</span>
|
||||||
|
<div class="action-group run-actions">
|
||||||
|
<button id="format" disabled>Format</button>
|
||||||
|
<label class="entry-picker">
|
||||||
|
<span>Entry</span>
|
||||||
|
<select id="entry-select" aria-label="Entry point file"></select>
|
||||||
|
</label>
|
||||||
<button id="run" disabled>Run</button>
|
<button id="run" disabled>Run</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="status" class="status">Loading WASM...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="playground-shell">
|
<div class="playground-shell">
|
||||||
|
|||||||
@@ -1,173 +1,392 @@
|
|||||||
export const playgroundExamples = [
|
export const playgroundExamples = [
|
||||||
{
|
{
|
||||||
id: 'service-deployment',
|
id: 'product-api-production',
|
||||||
title: 'Service deployment',
|
title: 'Production API deployment',
|
||||||
activePath: 'main.dcdl',
|
activePath: 'main.dcdl',
|
||||||
|
entryPath: 'main.dcdl',
|
||||||
files: {
|
files: {
|
||||||
'main.dcdl': `let
|
'main.dcdl': `let
|
||||||
schema = import "./schemas/service.dcdl";
|
schema = import "./schemas/service.dcdl";
|
||||||
prod = import "./env/prod.dcdl";
|
env = import "./env/production.dcdl";
|
||||||
shared = import "./shared/tags.dcdl";
|
org = import "./org/platform.dcdl";
|
||||||
|
release = import "./release.dcdl";
|
||||||
in
|
in
|
||||||
schema.Service & prod.ServicePatch & {
|
schema.Service & env.Production & {
|
||||||
name = "api";
|
name = "checkout-api";
|
||||||
image = "registry.example.com/api:2026-06-24";
|
image = release.image;
|
||||||
port = 9000 + 443;
|
owner = org.teams.checkout;
|
||||||
public = prod.is_public && 9000 + 443 > 9000;
|
port = 8443;
|
||||||
tags = shared.common ++ ["api", "edge"];
|
replicas = env.capacity.base_replicas + 2;
|
||||||
|
resources = {
|
||||||
|
cpu_milli = env.capacity.cpu_milli;
|
||||||
|
memory_mb = env.capacity.memory_mb;
|
||||||
|
};
|
||||||
|
autoscaling = {
|
||||||
|
enabled = true;
|
||||||
|
min = env.capacity.base_replicas;
|
||||||
|
max = env.capacity.base_replicas * 4;
|
||||||
|
};
|
||||||
|
rollout = {
|
||||||
|
strategy = match release.risk {
|
||||||
|
"high": "canary";
|
||||||
|
"medium": "rolling";
|
||||||
|
_: "all-at-once";
|
||||||
|
};
|
||||||
|
canary_percent = match release.risk {
|
||||||
|
"high": 10;
|
||||||
|
_: 100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ingress.hosts = env.domains.public ++ ["checkout.internal.example.com"];
|
||||||
|
labels = org.common_labels ++ ["service:checkout", "tier:edge"];
|
||||||
|
alerts.pager = org.pagers.checkout;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
'schemas/service.dcdl': `Service = {
|
'schemas/service.dcdl': `Service = {
|
||||||
name = String;
|
name = String;
|
||||||
image = String;
|
image = String;
|
||||||
port = Int & > 443 default 8443;
|
owner = String;
|
||||||
|
port = Int & >= 1024 & <= 65535 default 8080;
|
||||||
replicas = Int & > 0 default 2;
|
replicas = Int & > 0 default 2;
|
||||||
public = Bool default false;
|
resources = {
|
||||||
feature.metrics = Bool default true;
|
cpu_milli = Int & >= 100 default 500;
|
||||||
feature.tracing = Bool default false;
|
memory_mb = Int & >= 128 default 512;
|
||||||
|
};
|
||||||
|
autoscaling = {
|
||||||
|
enabled = Bool default false;
|
||||||
|
min = Int & > 0 default 2;
|
||||||
|
max = Int & > 0 default 4;
|
||||||
|
};
|
||||||
|
rollout = {
|
||||||
|
strategy = String default "rolling";
|
||||||
|
canary_percent = Int & >= 0 & <= 100 default 100;
|
||||||
|
};
|
||||||
|
observability = {
|
||||||
|
metrics = Bool default true;
|
||||||
|
tracing = Bool default true;
|
||||||
|
logs = Bool default true;
|
||||||
|
};
|
||||||
|
network.public = Bool default false;
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'env/prod.dcdl': `is_public = true;
|
'env/production.dcdl': `capacity = {
|
||||||
|
base_replicas = 6;
|
||||||
|
cpu_milli = 2000;
|
||||||
|
memory_mb = 4096;
|
||||||
|
};
|
||||||
|
|
||||||
ServicePatch = {
|
domains.public = ["checkout.example.com", "api.example.com"];
|
||||||
replicas = 3;
|
|
||||||
feature.tracing = true;
|
Production = {
|
||||||
|
network.public = true;
|
||||||
|
observability = {
|
||||||
|
tracing = true;
|
||||||
|
logs = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'shared/tags.dcdl': `common = ["decodal", "prod"];
|
'org/platform.dcdl': `teams = {
|
||||||
|
checkout = "team:checkout-platform";
|
||||||
|
platform = "team:core-platform";
|
||||||
|
};
|
||||||
|
|
||||||
|
pagers = {
|
||||||
|
checkout = "pagerduty:checkout-primary";
|
||||||
|
};
|
||||||
|
|
||||||
|
common_labels = ["managed-by:decodal", "environment:production"];
|
||||||
|
`,
|
||||||
|
'release.dcdl': `version = "2026.06.15";
|
||||||
|
risk = "high";
|
||||||
|
image = "registry.example.com/checkout-api:2026.06.15";
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'access-policy',
|
id: 'tenant-enterprise-plan',
|
||||||
title: 'Access policy',
|
title: 'Enterprise tenant contract',
|
||||||
activePath: 'main.dcdl',
|
activePath: 'main.dcdl',
|
||||||
|
entryPath: 'main.dcdl',
|
||||||
files: {
|
files: {
|
||||||
'main.dcdl': `let
|
'main.dcdl': `let
|
||||||
policy = import "./policy/base.dcdl";
|
schema = import "./schemas/tenant.dcdl";
|
||||||
teams = import "./policy/teams.dcdl";
|
plans = import "./plans.dcdl";
|
||||||
env = "prod";
|
region = import "./regions/eu.dcdl";
|
||||||
|
customer = import "./customers/acme.dcdl";
|
||||||
in
|
in
|
||||||
{
|
schema.Tenant & (plans.Enterprise // region.Europe // customer.Acme // {
|
||||||
service = "payments";
|
slug = "acme";
|
||||||
readers = policy.defaultReaders ++ teams.observability;
|
display_name = "Acme Manufacturing";
|
||||||
writers = policy.defaultWriters ++ teams.payments;
|
limits = {
|
||||||
admins = match env {
|
users = customer.seats + 25;
|
||||||
"prod": policy.breakglassAdmins;
|
projects = 200;
|
||||||
_: teams.platform;
|
storage_gb = 2048;
|
||||||
};
|
};
|
||||||
audit.required = env == "prod";
|
features = {
|
||||||
audit.retention_days = match env {
|
audit_log = true;
|
||||||
"prod": 365;
|
sso = true;
|
||||||
_: 30;
|
data_residency = true;
|
||||||
};
|
};
|
||||||
}
|
compliance.retention_days = match customer.contract_tier {
|
||||||
|
"regulated": 2555;
|
||||||
|
_: 365;
|
||||||
|
};
|
||||||
|
notifications.channels = customer.notification_channels ++ ["security"];
|
||||||
|
})
|
||||||
`,
|
`,
|
||||||
'policy/base.dcdl': `defaultReaders = ["group:engineering", "group:support"];
|
'schemas/tenant.dcdl': `Tenant = {
|
||||||
defaultWriters = ["group:platform"];
|
slug = String;
|
||||||
breakglassAdmins = ["user:oncall-primary", "user:oncall-secondary"];
|
display_name = String;
|
||||||
|
plan = String default "standard";
|
||||||
|
limits = {
|
||||||
|
users = Int & > 0 default 25;
|
||||||
|
projects = Int & > 0 default 20;
|
||||||
|
storage_gb = Int & > 0 default 100;
|
||||||
|
};
|
||||||
|
features = {
|
||||||
|
sso = Bool default false;
|
||||||
|
audit_log = Bool default true;
|
||||||
|
data_residency = Bool default false;
|
||||||
|
};
|
||||||
|
compliance = {
|
||||||
|
region = String default "us";
|
||||||
|
retention_days = Int & >= 30 default 90;
|
||||||
|
};
|
||||||
|
billing = {
|
||||||
|
currency = String default "USD";
|
||||||
|
invoice_day = Int & >= 1 & <= 28 default 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
`,
|
`,
|
||||||
'policy/teams.dcdl': `platform = ["group:platform-admins"];
|
'plans.dcdl': `Enterprise = {
|
||||||
payments = ["group:payments-api", "group:payments-sre"];
|
plan = "enterprise";
|
||||||
observability = ["group:observability"];
|
limits = {
|
||||||
|
users = 500;
|
||||||
|
projects = 100;
|
||||||
|
storage_gb = 1024;
|
||||||
|
};
|
||||||
|
features = {
|
||||||
|
sso = true;
|
||||||
|
audit_log = true;
|
||||||
|
};
|
||||||
|
billing.invoice_day = 15;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'regions/eu.dcdl': `Europe = {
|
||||||
|
compliance = {
|
||||||
|
region = "eu-west";
|
||||||
|
retention_days = 365;
|
||||||
|
};
|
||||||
|
billing.currency = "EUR";
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'customers/acme.dcdl': `seats = 475;
|
||||||
|
contract_tier = "regulated";
|
||||||
|
notification_channels = ["email", "slack", "pagerduty"];
|
||||||
|
|
||||||
|
Acme = {
|
||||||
|
billing.invoice_day = 10;
|
||||||
|
};
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'edge-worker',
|
id: 'event-pipeline-prod',
|
||||||
title: 'Edge worker config',
|
title: 'Event ingestion pipeline',
|
||||||
activePath: 'main.dcdl',
|
|
||||||
files: {
|
|
||||||
'main.dcdl': `let
|
|
||||||
schema = import "./schemas/worker.dcdl";
|
|
||||||
routes = import "./routes.dcdl";
|
|
||||||
production = true;
|
|
||||||
in
|
|
||||||
schema.Worker & {
|
|
||||||
name = "decodal-docs";
|
|
||||||
compatibility_date = "2026-06-15";
|
|
||||||
workers_dev = !production;
|
|
||||||
route.host = routes.primary_host;
|
|
||||||
route.paths = routes.docs_paths ++ routes.playground_paths;
|
|
||||||
cache.ttl_seconds = 60 * 60;
|
|
||||||
cache.bypass = !production;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
'schemas/worker.dcdl': `Worker = {
|
|
||||||
name = String;
|
|
||||||
compatibility_date = String;
|
|
||||||
workers_dev = Bool default false;
|
|
||||||
route.host = String;
|
|
||||||
cache.ttl_seconds = Int & >= 0 default 300;
|
|
||||||
cache.bypass = Bool default false;
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'routes.dcdl': `primary_host = "decodal.example.com";
|
|
||||||
docs_paths = ["/docs/*", "/assets/*"];
|
|
||||||
playground_paths = ["/playground/*"];
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data-pipeline',
|
|
||||||
title: 'Data pipeline',
|
|
||||||
activePath: 'main.dcdl',
|
activePath: 'main.dcdl',
|
||||||
|
entryPath: 'main.dcdl',
|
||||||
files: {
|
files: {
|
||||||
'main.dcdl': `let
|
'main.dcdl': `let
|
||||||
schema = import "./schemas/pipeline.dcdl";
|
schema = import "./schemas/pipeline.dcdl";
|
||||||
presets = import "./presets/batch.dcdl";
|
env = import "./env/prod.dcdl";
|
||||||
env = "staging";
|
sources = import "./sources/events.dcdl";
|
||||||
|
sinks = import "./sinks/warehouse.dcdl";
|
||||||
|
transforms = import "./transforms.dcdl";
|
||||||
in
|
in
|
||||||
schema.Pipeline & presets.Batch & {
|
schema.Pipeline & env.Production & {
|
||||||
name = "events-rollup";
|
name = "events-rollup";
|
||||||
source.topic = "events.raw";
|
source = {
|
||||||
sink.table = "analytics.events_daily";
|
kafka_cluster = sources.cluster;
|
||||||
workers = match env {
|
topic = sources.raw_topic;
|
||||||
"prod": 8;
|
consumer_group = "analytics-events-rollup";
|
||||||
"staging": 3;
|
|
||||||
_: 1;
|
|
||||||
};
|
};
|
||||||
alerts.enabled = env != "dev";
|
sink = {
|
||||||
transforms = presets.standardTransforms ++ ["dedupe", "aggregate_daily"];
|
table = sinks.daily_events_table;
|
||||||
|
mode = "append";
|
||||||
|
};
|
||||||
|
workers = env.scale.worker_count;
|
||||||
|
batch = {
|
||||||
|
size = env.scale.batch_size;
|
||||||
|
timeout_seconds = 60 + 30;
|
||||||
|
};
|
||||||
|
schedule.cron = "*/15 * * * *";
|
||||||
|
transforms = transforms.standard ++ transforms.privacy ++ ["aggregate_daily"];
|
||||||
|
quality.max_lag_seconds = match env.tier {
|
||||||
|
"gold": 120;
|
||||||
|
"silver": 300;
|
||||||
|
_: 900;
|
||||||
|
};
|
||||||
|
alerts.channels = env.alert_channels;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
'schemas/pipeline.dcdl': `Pipeline = {
|
'schemas/pipeline.dcdl': `Pipeline = {
|
||||||
name = String;
|
name = String;
|
||||||
source.topic = String;
|
source = {
|
||||||
sink.table = String;
|
kafka_cluster = String;
|
||||||
workers = Int & > 0 default 1;
|
topic = String;
|
||||||
batch.size = Int & >= 100 default 1000;
|
consumer_group = String;
|
||||||
batch.timeout_seconds = Int & > 0 default 60;
|
};
|
||||||
alerts.enabled = Bool default false;
|
sink = {
|
||||||
|
table = String;
|
||||||
|
mode = String default "append";
|
||||||
|
};
|
||||||
|
workers = Int & > 0 default 2;
|
||||||
|
batch = {
|
||||||
|
size = Int & >= 100 default 1000;
|
||||||
|
timeout_seconds = Int & > 0 default 60;
|
||||||
|
};
|
||||||
|
schedule.cron = String;
|
||||||
|
quality.max_lag_seconds = Int & > 0 default 300;
|
||||||
|
dead_letter = {
|
||||||
|
enabled = Bool default true;
|
||||||
|
topic = String default "events.dead_letter";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'presets/batch.dcdl': `standardTransforms = ["parse_json", "validate_schema", "enrich_metadata"];
|
'env/prod.dcdl': `tier = "gold";
|
||||||
|
alert_channels = ["slack:data-platform", "pagerduty:data-primary"];
|
||||||
|
|
||||||
Batch = {
|
scale = {
|
||||||
batch.size = 5000;
|
worker_count = 12;
|
||||||
batch.timeout_seconds = 30 + 30;
|
batch_size = 5000;
|
||||||
|
};
|
||||||
|
|
||||||
|
Production = {
|
||||||
|
dead_letter.enabled = true;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'sources/events.dcdl': `cluster = "kafka-prod-use1";
|
||||||
|
raw_topic = "events.raw.v3";
|
||||||
|
`,
|
||||||
|
'sinks/warehouse.dcdl': `daily_events_table = "analytics.events_daily";
|
||||||
|
`,
|
||||||
|
'transforms.dcdl': `standard = ["parse_json", "validate_schema", "dedupe"];
|
||||||
|
privacy = ["redact_ip", "hash_user_id"];
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edge-cdn-policy',
|
||||||
|
title: 'Edge CDN routing policy',
|
||||||
|
activePath: 'main.dcdl',
|
||||||
|
entryPath: 'main.dcdl',
|
||||||
|
files: {
|
||||||
|
'main.dcdl': `let
|
||||||
|
schema = import "./schemas/edge.dcdl";
|
||||||
|
domains = import "./domains.dcdl";
|
||||||
|
security = import "./security.dcdl";
|
||||||
|
env = "production";
|
||||||
|
in
|
||||||
|
schema.EdgeApplication & security.StrictHeaders & {
|
||||||
|
name = "docs-and-playground";
|
||||||
|
domains = domains.primary ++ domains.aliases;
|
||||||
|
origin = {
|
||||||
|
host = "origin-docs.internal.example.com";
|
||||||
|
port = 443;
|
||||||
|
};
|
||||||
|
tls.mode = "strict";
|
||||||
|
cache = {
|
||||||
|
default_ttl_seconds = 60 * 60;
|
||||||
|
static_ttl_seconds = 60 * 60 * 24;
|
||||||
|
bypass_preview = env != "production";
|
||||||
|
};
|
||||||
|
routing = {
|
||||||
|
docs_paths = ["/docs/*", "/assets/*"];
|
||||||
|
playground_paths = ["/playground/*"];
|
||||||
|
};
|
||||||
|
waf = {
|
||||||
|
enabled = true;
|
||||||
|
rules = security.managed_rules ++ ["block-country:kp", "rate-limit-login"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'schemas/edge.dcdl': `EdgeApplication = {
|
||||||
|
name = String;
|
||||||
|
origin = {
|
||||||
|
host = String;
|
||||||
|
port = Int & >= 1 & <= 65535 default 443;
|
||||||
|
};
|
||||||
|
tls.mode = String default "strict";
|
||||||
|
cache = {
|
||||||
|
default_ttl_seconds = Int & >= 0 default 300;
|
||||||
|
static_ttl_seconds = Int & >= 0 default 86400;
|
||||||
|
bypass_preview = Bool default false;
|
||||||
|
};
|
||||||
|
waf.enabled = Bool default true;
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'domains.dcdl': `primary = ["decodal.example.com"];
|
||||||
|
aliases = ["www.decodal.example.com", "docs.decodal.example.com"];
|
||||||
|
`,
|
||||||
|
'security.dcdl': `managed_rules = ["owasp-core", "known-bots", "credential-stuffing"];
|
||||||
|
|
||||||
|
StrictHeaders = {
|
||||||
|
headers = {
|
||||||
|
hsts = "max-age=31536000; includeSubDomains; preload";
|
||||||
|
referrer_policy = "same-origin";
|
||||||
|
frame_options = "DENY";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'diagnostics',
|
id: 'incident-escalation',
|
||||||
title: 'Diagnostics: invalid port',
|
title: 'Incident escalation policy',
|
||||||
activePath: 'main.dcdl',
|
activePath: 'main.dcdl',
|
||||||
|
entryPath: 'main.dcdl',
|
||||||
files: {
|
files: {
|
||||||
'main.dcdl': `let
|
'main.dcdl': `let
|
||||||
schema = import "./schemas/service.dcdl";
|
schema = import "./schemas/oncall.dcdl";
|
||||||
|
teams = import "./teams.dcdl";
|
||||||
|
calendar = import "./calendar.dcdl";
|
||||||
|
severity = "sev1";
|
||||||
in
|
in
|
||||||
schema.Service & {
|
schema.Policy & {
|
||||||
name = "broken-api";
|
service = "payments";
|
||||||
port = 442;
|
severity = severity;
|
||||||
|
primary = teams.payments_primary;
|
||||||
|
secondary = teams.platform_secondary;
|
||||||
|
notify = teams.payments_primary ++ teams.platform_secondary ++ calendar.executive_watch;
|
||||||
|
response_minutes = match severity {
|
||||||
|
"sev1": 5;
|
||||||
|
"sev2": 15;
|
||||||
|
_: 60;
|
||||||
|
};
|
||||||
|
escalation = {
|
||||||
|
after_minutes = match severity {
|
||||||
|
"sev1": 10;
|
||||||
|
"sev2": 30;
|
||||||
|
_: 120;
|
||||||
|
};
|
||||||
|
targets = teams.platform_secondary ++ calendar.executive_watch;
|
||||||
|
};
|
||||||
|
runbooks = ["runbooks/payments-api", "runbooks/database-failover"];
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
'schemas/service.dcdl': `Service = {
|
'schemas/oncall.dcdl': `Policy = {
|
||||||
name = String;
|
service = String;
|
||||||
port = Int & > 443 default 8443;
|
severity = String;
|
||||||
replicas = Int & > 0 default 2;
|
response_minutes = Int & > 0 default 30;
|
||||||
|
escalation.after_minutes = Int & > 0 default 60;
|
||||||
|
auto_page = Bool default true;
|
||||||
|
create_status_page = Bool default true;
|
||||||
};
|
};
|
||||||
|
`,
|
||||||
|
'teams.dcdl': `payments_primary = ["user:pay-oncall-a", "user:pay-oncall-b"];
|
||||||
|
platform_secondary = ["user:platform-sre-a", "user:platform-sre-b"];
|
||||||
|
`,
|
||||||
|
'calendar.dcdl': `executive_watch = ["user:vp-engineering", "user:cto"];
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
import init, { evaluateProject } from 'decodal-wasm';
|
import initRuntime, { evaluateProject } from 'decodal-wasm';
|
||||||
|
import runtimeWasmUrl from 'decodal-wasm/decodal_wasm_bg.wasm?url';
|
||||||
import { EditorView, basicSetup } from 'codemirror';
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
import { keymap } from '@codemirror/view';
|
import { keymap } from '@codemirror/view';
|
||||||
import { decodal } from 'decodal-codemirror';
|
import { decodal } from 'decodal-codemirror';
|
||||||
|
import { formatDecodal, initDecodalFormatter } from 'decodal-codemirror/format';
|
||||||
|
import toolsWasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
import { playgroundExamples } from './playground-examples.js';
|
import { playgroundExamples } from './playground-examples.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'decodal-playground-project-v1';
|
const STORAGE_KEY = 'decodal-playground-project-v2';
|
||||||
const starterProject = playgroundExamples[0];
|
const starterProject = playgroundExamples[0];
|
||||||
|
|
||||||
const editorHost = document.getElementById('editor');
|
const editorHost = document.getElementById('editor');
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
const run = document.getElementById('run');
|
const run = document.getElementById('run');
|
||||||
|
const formatButton = document.getElementById('format');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const fileTree = document.getElementById('file-tree');
|
const fileTree = document.getElementById('file-tree');
|
||||||
const activeFile = document.getElementById('active-file');
|
const activeFile = document.getElementById('active-file');
|
||||||
const newFile = document.getElementById('new-file');
|
const newFile = document.getElementById('new-file');
|
||||||
const deleteFile = document.getElementById('delete-file');
|
const deleteFile = document.getElementById('delete-file');
|
||||||
const exampleSelect = document.getElementById('example-select');
|
const exampleSelect = document.getElementById('example-select');
|
||||||
|
const entrySelect = document.getElementById('entry-select');
|
||||||
const loadExample = document.getElementById('load-example');
|
const loadExample = document.getElementById('load-example');
|
||||||
|
|
||||||
const project = loadProject();
|
const project = loadProject();
|
||||||
@@ -86,6 +91,7 @@ for (const example of playgroundExamples) {
|
|||||||
exampleSelect.value = starterProject.id;
|
exampleSelect.value = starterProject.id;
|
||||||
|
|
||||||
setActiveFile(project.activePath);
|
setActiveFile(project.activePath);
|
||||||
|
updateRunLabel();
|
||||||
renderFileTree();
|
renderFileTree();
|
||||||
|
|
||||||
function loadProject() {
|
function loadProject() {
|
||||||
@@ -94,12 +100,22 @@ function loadProject() {
|
|||||||
if (stored && stored.files && typeof stored.activePath === 'string') {
|
if (stored && stored.files && typeof stored.activePath === 'string') {
|
||||||
const files = normalizeFiles(stored.files);
|
const files = normalizeFiles(stored.files);
|
||||||
const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath;
|
const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath;
|
||||||
if (activePath) return { files, activePath };
|
if (activePath) {
|
||||||
|
const storedEntryPath = typeof stored.entryPath === 'string' ? normalizePath(stored.entryPath) : '';
|
||||||
|
const entryPath = files[storedEntryPath] === undefined
|
||||||
|
? files['main.dcdl'] === undefined ? activePath : 'main.dcdl'
|
||||||
|
: storedEntryPath;
|
||||||
|
return { files, activePath, entryPath };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Fall back to the starter project.
|
// Fall back to the starter project.
|
||||||
}
|
}
|
||||||
return { files: cloneFiles(starterProject.files), activePath: starterProject.activePath };
|
return {
|
||||||
|
files: cloneFiles(starterProject.files),
|
||||||
|
activePath: starterProject.activePath,
|
||||||
|
entryPath: starterProject.entryPath ?? starterProject.activePath,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneFiles(files) {
|
function cloneFiles(files) {
|
||||||
@@ -121,10 +137,13 @@ function saveProject() {
|
|||||||
|
|
||||||
function loadExampleProject(exampleId) {
|
function loadExampleProject(exampleId) {
|
||||||
const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject;
|
const example = playgroundExamples.find((item) => item.id === exampleId) ?? starterProject;
|
||||||
project.files = cloneFiles(example.files);
|
const files = cloneFiles(example.files);
|
||||||
project.activePath = example.activePath;
|
project.files = {};
|
||||||
|
project.activePath = '';
|
||||||
|
project.entryPath = example.entryPath ?? example.activePath;
|
||||||
|
project.files = files;
|
||||||
exampleSelect.value = example.id;
|
exampleSelect.value = example.id;
|
||||||
setActiveFile(project.activePath);
|
setActiveFile(example.activePath);
|
||||||
output.textContent = '';
|
output.textContent = '';
|
||||||
output.classList.remove('error');
|
output.classList.remove('error');
|
||||||
execute();
|
execute();
|
||||||
@@ -165,17 +184,56 @@ function setActiveFile(path) {
|
|||||||
activeFile.textContent = normalized;
|
activeFile.textContent = normalized;
|
||||||
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
deleteFile.disabled = Object.keys(project.files).length <= 1;
|
||||||
renderFileTree();
|
renderFileTree();
|
||||||
|
updateRunLabel();
|
||||||
saveProject();
|
saveProject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateEntrySelect() {
|
||||||
|
const paths = Object.keys(project.files).sort();
|
||||||
|
const entryPath = project.files[project.entryPath] === undefined ? project.activePath : project.entryPath;
|
||||||
|
project.entryPath = entryPath;
|
||||||
|
entrySelect.replaceChildren(
|
||||||
|
...paths.map((path) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = path;
|
||||||
|
option.textContent = path;
|
||||||
|
return option;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
entrySelect.value = entryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRunLabel() {
|
||||||
|
updateEntrySelect();
|
||||||
|
run.textContent = 'Run';
|
||||||
|
run.title = `Materialize ${project.entryPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
function execute() {
|
function execute() {
|
||||||
project.files[project.activePath] = getEditorText();
|
project.files[project.activePath] = getEditorText();
|
||||||
|
const entryPath = project.files[project.entryPath] === undefined ? project.activePath : project.entryPath;
|
||||||
|
project.entryPath = entryPath;
|
||||||
|
updateRunLabel();
|
||||||
saveProject();
|
saveProject();
|
||||||
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
|
const result = JSON.parse(evaluateProject(entryPath, JSON.stringify(project.files)));
|
||||||
output.textContent = result.ok ? result.output : result.error;
|
output.textContent = result.ok ? result.output : result.error;
|
||||||
output.classList.toggle('error', !result.ok);
|
output.classList.toggle('error', !result.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatActiveFile() {
|
||||||
|
const result = formatDecodal(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() {
|
function renderFileTree() {
|
||||||
const tree = buildTree(Object.keys(project.files).sort());
|
const tree = buildTree(Object.keys(project.files).sort());
|
||||||
fileTree.replaceChildren(renderTreeList(tree.children));
|
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||||
@@ -227,19 +285,27 @@ function compareNodes(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await init();
|
await Promise.all([initRuntime(runtimeWasmUrl), initDecodalFormatter(toolsWasmUrl)]);
|
||||||
run.disabled = false;
|
run.disabled = false;
|
||||||
|
formatButton.disabled = false;
|
||||||
status.textContent = '';
|
status.textContent = '';
|
||||||
execute();
|
execute();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
|
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entrySelect.addEventListener('change', () => {
|
||||||
|
const path = normalizePath(entrySelect.value);
|
||||||
|
if (project.files[path] === undefined) return;
|
||||||
|
project.entryPath = path;
|
||||||
|
updateRunLabel();
|
||||||
|
saveProject();
|
||||||
|
});
|
||||||
run.addEventListener('click', execute);
|
run.addEventListener('click', execute);
|
||||||
|
formatButton.addEventListener('click', formatActiveFile);
|
||||||
loadExample.addEventListener('click', () => {
|
loadExample.addEventListener('click', () => {
|
||||||
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
||||||
if (!example) return;
|
if (!example) return;
|
||||||
if (!confirm(`Load example "${example.title}"? This replaces the current virtual files.`)) return;
|
|
||||||
loadExampleProject(example.id);
|
loadExampleProject(example.id);
|
||||||
});
|
});
|
||||||
newFile.addEventListener('click', () => {
|
newFile.addEventListener('click', () => {
|
||||||
@@ -256,5 +322,8 @@ deleteFile.addEventListener('click', () => {
|
|||||||
if (Object.keys(project.files).length <= 1) return;
|
if (Object.keys(project.files).length <= 1) return;
|
||||||
if (!confirm(`Delete ${project.activePath}?`)) return;
|
if (!confirm(`Delete ${project.activePath}?`)) return;
|
||||||
delete project.files[project.activePath];
|
delete project.files[project.activePath];
|
||||||
|
if (project.files[project.entryPath] === undefined) {
|
||||||
|
project.entryPath = Object.keys(project.files).sort()[0];
|
||||||
|
}
|
||||||
setActiveFile(Object.keys(project.files).sort()[0]);
|
setActiveFile(Object.keys(project.files).sort()[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -293,11 +293,23 @@ main.playground {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-picker {
|
.action-group {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-separator {
|
||||||
|
color: var(--border);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-picker,
|
||||||
|
.entry-picker {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -307,17 +319,23 @@ main.playground {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-picker select {
|
.example-picker select,
|
||||||
|
.entry-picker select {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
min-height: 28px;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
padding: 7px 10px;
|
padding: 4px 10px;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-picker select {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--link);
|
background: var(--link);
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -325,7 +343,8 @@ button {
|
|||||||
color: var(--topbar-text);
|
color: var(--topbar-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 8px 14px;
|
min-height: 28px;
|
||||||
|
padding: 4px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@@ -519,45 +538,57 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-packages {
|
.home-packages {
|
||||||
|
align-items: flex-start;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.9rem 1.4rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
padding: 1.4rem;
|
padding: 1.2rem 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-packages h2 {
|
.home-packages h2 {
|
||||||
margin-top: 0;
|
flex-basis: 100%;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-links {
|
.primary-package {
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-links a {
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1rem;
|
flex: 0 1 320px;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-links span {
|
.primary-package span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-links strong {
|
.primary-package strong {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.95rem;
|
font-size: 1.15rem;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.package-list {
|
||||||
|
columns: 1;
|
||||||
|
flex: 1 1 320px;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-list li {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.home-grid {
|
.home-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -604,10 +635,12 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
.playground-shell,
|
.playground-shell,
|
||||||
.home-grid,
|
.home-grid,
|
||||||
.home-example,
|
.home-example {
|
||||||
.package-links {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.package-list {
|
||||||
|
columns: 1;
|
||||||
|
}
|
||||||
.hero {
|
.hero {
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user