diff --git a/.github/workflows/build_latest.yml b/.github/workflows/build_latest.yml index 7a0645da8..9fb99a895 100644 --- a/.github/workflows/build_latest.yml +++ b/.github/workflows/build_latest.yml @@ -11,6 +11,7 @@ on: - "site/**" - "pad/**" - "tests*/**" + - "generator/**" - "changelog.md" - "readme.md" - ".github/workflows/build_latest.yml" @@ -103,6 +104,7 @@ jobs: run: | rustup target add wasm32-unknown-unknown cargo install trunk --locked --root target + cargo install --release --locked --package uiua-generator --root target - name: Build site run: | git checkout site @@ -110,9 +112,12 @@ jobs: git reset --hard origin/site git rebase main || { echo "Rebase failed, aborting."; git rebase --abort; exit 1; } cd site - cargo test -p site gen_blog_html + ../target/bin/uiua-generator primitives-json ./primitives.json + ../target/bin/uiua-generator format-config-docs ./text/format_config.md + ../target/bin/uiua-generator blog-html ./blog/ ../target/bin/trunk build --release -d ../docs git add --all + git add --force primitive.json text/format_config.md git commit --amend --no-edit git push --force git checkout main diff --git a/.gitignore b/.gitignore index 630727fc6..a941f99ab 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ uiua.tmLanguage.json # symlink created by `nix build` result -.direnv \ No newline at end of file +.direnv + +site/primitives.json +site/text/format_config.md diff --git a/Cargo.lock b/Cargo.lock index d48ed1ae9..6f1f37513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1062,9 +1062,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -1072,9 +1072,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -1085,9 +1085,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -6593,6 +6593,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "uiua-generator" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "comrak", + "either", + "serde", + "serde_json", + "site", + "uiua", +] + [[package]] name = "uiua_parser" version = "0.17.0-dev.2" diff --git a/Cargo.toml b/Cargo.toml index a9520bcc4..9e9336acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["site", "tests_ffi", "pad/editor", "parser"] +members = ["site", "tests_ffi", "pad/editor", "parser", "generator"] [workspace.package] categories = ["compilers", "algorithms", "multimedia", "parser-implementations"] @@ -243,6 +243,7 @@ web = ["wasm-bindgen", "js-sys", "web-sys"] webcam = ["image", "nokhwa"] window = ["eframe", "rmp-serde", "image", "native-dialog"] xlsx = ["calamine", "simple_excel_writer"] +internal_fmt_doc = [] # Use system static libraries instead of building them system = ["libffi?/system"] diff --git a/flake.nix b/flake.nix index 6c98e529e..4de9c1bc0 100644 --- a/flake.nix +++ b/flake.nix @@ -61,8 +61,15 @@ }; packages = { default = pkgs.callPackage ./nix/package.nix { inherit craneLib libPath rustPlatform; }; + generator = pkgs.callPackage ./nix/generator.nix { inherit craneLib; }; fonts = pkgs.callPackage ./nix/fonts.nix { }; - site = pkgs.callPackage ./nix/site.nix { inherit craneLib; }; + site = pkgs.callPackage ./nix/site.nix { + inherit craneLib; + inherit (self'.packages) generator; + }; + sublime-grammar = pkgs.callPackage ./nix/sublime-grammar.nix { + inherit (self'.packages) generator; + }; toolchain = toolchainFor pkgs; }; apps.site.program = pkgs.writeShellApplication { diff --git a/generator/Cargo.toml b/generator/Cargo.toml new file mode 100644 index 000000000..1f99a7966 --- /dev/null +++ b/generator/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "uiua-generator" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +clap = { version = "4.5.41", features = ["derive"] } +comrak = "0.39.0" +either = "1.15.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +site = { path = "../site", default-features = false, features = ["gen_blog_html"] } +uiua = { path = "..", default-features = false, features = [ + "internal_fmt_doc", +] } diff --git a/generator/src/main.rs b/generator/src/main.rs new file mode 100644 index 000000000..7c0cc018f --- /dev/null +++ b/generator/src/main.rs @@ -0,0 +1,437 @@ +use std::{ + borrow::Cow, + collections::BTreeMap, + fmt::Display, + fs::{self, OpenOptions}, + io::{Read, Write}, + ops::Not, + path::{Component, Path, PathBuf}, + str::FromStr, +}; + +use clap::{Parser, Subcommand}; +use either::Either; +use serde::Serialize; +use serde_json::json; +use site::markdown::markdown_html; +use uiua::{PrimClass, PrimDoc, Primitive}; + +#[derive(Parser, Debug)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Generate primitives.json file + PrimitivesJson { + /// Where to write the generated JSON + #[arg(default_value_t)] + output: StdioFile, + }, + /// Generate uiua.tmLanguage.json file + SublimeGrammar { + /// Where to write the generated JSON + #[arg(default_value_t)] + output: StdioFile, + }, + /// Generate formatter configuration documentation + FormatConfigDocs { + /// Where to write the generated documentation + #[arg(default_value_t)] + output: StdioFile, + }, + /// Render a Markdown page to HTML + BlogPageHtml { + /// Slug of the page + slug: String, + /// Markdown file + #[arg(default_value_t)] + source: StdioFile, + /// HTML file + #[arg(default_value_t)] + dest: StdioFile, + }, + /// Render multiple blog pages to HTML + BlogHtml { + /// Directory containing source and target files + dir: PathBuf, + /// Page list; defaults to `list.txt` inside the provided directory + list: Option, + }, +} + +#[derive(Clone, Debug, Default)] +enum StdioFile { + #[default] + Stdio, + File(PathBuf), +} + +impl FromStr for StdioFile { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + if s == "-" { + Ok(Self::Stdio) + } else { + s.parse().map(Self::File) + } + } +} + +impl Display for StdioFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Stdio => write!(f, "-"), + Self::File(path_buf) => { + if let Some(Component::Normal(_)) = path_buf.components().next() { + write!(f, "./{}", path_buf.display()) + } else { + write!(f, "{}", path_buf.display()) + } + } + } + } +} + +impl StdioFile { + fn writer(&self) -> std::io::Result { + match self { + Self::Stdio => Ok(Either::Left(std::io::stdout())), + Self::File(path) => OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map(Either::Right), + } + } + fn reader(&self) -> std::io::Result { + match self { + StdioFile::Stdio => Ok(Either::Left(std::io::stdin())), + StdioFile::File(path) => OpenOptions::new().read(true).open(path).map(Either::Right), + } + } +} + +fn main() -> anyhow::Result<()> { + let Cli { command } = Cli::parse(); + match command { + Command::PrimitivesJson { output } => { + serde_json::to_writer_pretty(output.writer()?, &primitives())?; + } + Command::SublimeGrammar { output } => { + serde_json::to_writer_pretty(output.writer()?, &sublime_grammar())?; + } + Command::FormatConfigDocs { output } => { + output + .writer()? + .write_all(uiua::format::format_cfg_docs().as_bytes())?; + } + Command::BlogPageHtml { source, dest, slug } => { + let mut buf = String::new(); + source.reader()?.read_to_string(&mut buf)?; + dest.writer()? + .write_all(process_md(&buf, &slug).as_bytes())?; + } + Command::BlogHtml { dir, list } => { + let mut buf = String::new(); + list.unwrap_or_else(|| StdioFile::File(dir.join("list.txt"))) + .reader()? + .read_to_string(&mut buf)?; + gen_blog_html(&dir, &buf)?; + } + } + Ok(()) +} + +fn primitives() -> impl Serialize { + #[derive(Serialize)] + struct PrimDef { + #[serde(skip_serializing_if = "Option::is_none")] + ascii: Option, + #[serde(skip_serializing_if = "Option::is_none")] + glyph: Option, + #[serde(skip_serializing_if = "Option::is_none")] + args: Option, + #[serde(skip_serializing_if = "Option::is_none")] + outputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + modifier_args: Option, + class: String, + description: String, + #[serde(skip_serializing_if = "Not::not")] + experimental: bool, + #[serde(skip_serializing_if = "Not::not")] + deprecated: bool, + } + + Primitive::all() + .map(|prim| { + ( + prim.name(), + PrimDef { + ascii: prim.ascii().map(|a| a.to_string()), + glyph: prim.glyph(), + args: prim.args(), + outputs: prim.outputs(), + modifier_args: prim.modifier_args(), + class: format!("{:?}", prim.class()), + description: PrimDoc::from(prim).short_text().into_owned(), + experimental: prim.is_experimental(), + deprecated: prim.is_deprecated(), + }, + ) + }) + .collect::>() +} + +fn sublime_grammar() -> impl Serialize { + fn gen_group<'a>( + prims: impl IntoIterator, + additional: &str, + ) -> String { + let prims = prims.into_iter(); + let glyphs = prims + .clone() + .flat_map(|p| { + p.glyph().map(|x| x.to_string()).into_iter().chain( + p.ascii() + .into_iter() + .map(|x| x.to_string()) + .filter(|x| x.len() == 1), + ) + }) + .map(|x| match x.as_str() { + r"\" | "-" | "*" | "^" => format!(r"\{x}"), + _ => x, + }) + .collect::(); + let names = prims + .map(|p| { + let name = p.name(); + let min_len = if name.starts_with('&') { + name.len() + } else { + (2..=name.len()) + .find(|&n| Primitive::from_format_name(&name[..n]) == Some(*p)) + .unwrap() + }; + let mut chars = name.chars(); + let pref: String = chars.by_ref().take(min_len).collect(); + let re: String = chars + .clone() + .flat_map(|c| ['(', c]) + .chain(std::iter::repeat_n([')', '?'], chars.count()).flatten()) + .collect(); + format!("{pref}{re}") + }) + .collect::>() + .join("|"); + format!(r#"[{glyphs}]|(?, + funcs: [Vec; 4], + mods: [Vec; 3], + } + + let PrimGroups { stack, funcs, mods } = Primitive::non_deprecated() + .filter(|x| x.class() != PrimClass::Constant) + .fold(PrimGroups::default(), |mut acc, cur| { + match (cur.args(), cur.modifier_args(), cur.class()) { + (_, Some(x), _) => acc.mods.get_mut(x), + (Some(_), None, PrimClass::Stack | PrimClass::Debug | PrimClass::Planet) => { + Some(&mut acc.stack) + } + (Some(x), None, _) => acc.funcs.get_mut(x), + (None, None, _) => panic!("Expected {cur:?} to be a function or a modifier"), + } + .unwrap_or_else(|| { + panic!( + "Couldn't fit {cur:?} (takes {:?} args and {:?} modifier args)", + cur.args(), + cur.modifier_args() + ) + }) + .push(cur); + acc + }); + assert_eq!(mods[0], [], "noadic modifiers don't exist"); + let stack_functions = gen_group(&stack, ""); + let noadic_functions = gen_group(&funcs[0], ""); + let monadic_functions = gen_group(&funcs[1], "|⋊[a-zA-Z]*"); + let dyadic_functions = gen_group(&funcs[2], ""); + let triadic_functions = gen_group(&funcs[3], ""); + let monadic_modifiers = gen_group(&mods[1], ""); + let dyadic_modifiers = gen_group(&mods[2], ""); + + json!({ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Uiua", + "firstLineMatch": r#"^#!/.*\buiua\b"#, + "scopeName": "source.uiua", + "fileTypes": ["ua"], + "patterns": [ + { "include": "#comments" }, + { "include": "#strings-multiline-format" }, + { "include": "#strings-multiline" }, + { "include": "#strings-format" }, + { "include": "#strings-normal" }, + { "include": "#characters" }, + { "include": "#labels" }, + { "include": "#module_delim" }, + { "include": "#strand" }, + { "include": "#stack" }, + { "include": "#noadic" }, + { "include": "#monadic" }, + { "include": "#dyadic" }, + { "include": "#triadic" }, + { "include": "#mod1" }, + { "include": "#mod2" }, + { "include": "#idents" }, + { "include": "#numbers" }, + ], + "repository": { + "idents": { + "name": "variable.parameter.uiua", + "match": r#"\b[a-zA-Z]+(₋?[₀₁₂₃₄₅₆₇₈₉]|,`?\d+)*[!‼]*\b"#, + }, + "comments": { + "name": "comment.line.uiua", + "match": r#"(#.*$|$[a-zA-Z]*)"#, + }, + "strings-normal": { + "name": "constant.character.escape", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "string.quoted", + "match": r#"\\[\\"0nrt]"#, + }, + ], + }, + "strings-format": { + "name": "constant.character.escape", + "begin": r#"\$""#, + "end": r#"""#, + "patterns": [ + { + "name": "string.quoted", + "match": r#"\\[\\"0nrt_]"#, + }, + { + "name": "constant.numeric", + "match": r#"(? String { + let mut lines: Vec> = content.lines().map(Cow::Borrowed).collect(); + lines.insert( + 0, + Cow::Borrowed("[Uiua](https://uiua.org)\n\n[Blog Home](https://uiua.org/blog)"), + ); + lines.insert( + 3, + Cow::Owned(format!( + "\n\n**You can read this post with full editor \ + features [here](https://uiua.org/blog/{slug}).**\n\n" + )), + ); + markdown_html(&lines.join("\n")) +} + +fn gen_blog_html(dir: &Path, list: &str) -> std::io::Result<()> { + for slug in list + .lines() + .filter(|line| !(line.is_empty() || line.starts_with('#'))) + .map(|x| { + x.split_once('(') + .expect("slug should be followed by an opening parenthesis") + .0 + }) + { + let md_path = dir.join(format!("{slug}-text.md")); + let html_path = dir.join(format!("{slug}-html.html")); + eprintln!( + "{slug}: {} -> {}...", + md_path.display(), + html_path.display() + ); + let markdown = fs::read_to_string(&md_path)?; + fs::write(html_path, process_md(&markdown, slug))?; + } + Ok(()) +} diff --git a/nix/generator.nix b/nix/generator.nix new file mode 100644 index 000000000..a0b4f9c66 --- /dev/null +++ b/nix/generator.nix @@ -0,0 +1,28 @@ +{ + craneLib, + lib, +}: +let + commonArgs = { + pname = "uiua-generator"; + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.unions [ + (lib.fileset.fromSource (craneLib.cleanCargoSource ../.)) + ../src/assets + ../generator + # TODO: isolate further + ../changelog.md + ../site/blog/list.txt + ../site/text/idioms.ua + ]; + }; + strictDeps = true; + cargoExtraArgs = "--package uiua-generator"; + }; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + totalArgs = commonArgs // { + inherit cargoArtifacts; + }; +in +craneLib.buildPackage totalArgs diff --git a/nix/package.nix b/nix/package.nix index e22cacf12..77c60266a 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -42,7 +42,6 @@ let cargoArtifacts = craneLib.buildDepsOnly commonArgs; totalArgs = commonArgs // { inherit cargoArtifacts; - cargoTestExtraArgs = "-- --skip format::generate_format_cfg_docs"; postInstall = '' wrapProgram "$out/bin/uiua" --prefix LD_LIBRARY_PATH : "${libPath}" ''; diff --git a/nix/site.nix b/nix/site.nix index 0e9a81960..cdd1ea840 100644 --- a/nix/site.nix +++ b/nix/site.nix @@ -2,6 +2,7 @@ craneLib, lib, wasm-bindgen-cli_0_2_93, + generator, doCheck ? true, }: let @@ -27,8 +28,12 @@ let cargoArtifacts = craneLib.buildDepsOnly commonArgs; totalArgs = commonArgs // { inherit cargoArtifacts; + nativeBuildInputs = [ generator ]; preBuild = '' cd ./site/ + uiua-generator primitives-json ./primitives.json + uiua-generator format-config-docs ./text/format_config.md + uiua-generator blog-html ./blog/ ''; postBuild = '' cd .. diff --git a/nix/sublime-grammar.nix b/nix/sublime-grammar.nix new file mode 100644 index 000000000..ef7d047a0 --- /dev/null +++ b/nix/sublime-grammar.nix @@ -0,0 +1,7 @@ +{ + runCommand, + generator, +}: +runCommand "uiua.tmLanguage.json" { nativeBuildInputs = [ generator ]; } '' + uiua-generator sublime-grammar "$out" +'' diff --git a/pad/editor/Cargo.toml b/pad/editor/Cargo.toml index ba274dd73..560bfaea6 100644 --- a/pad/editor/Cargo.toml +++ b/pad/editor/Cargo.toml @@ -18,7 +18,7 @@ workspace = true [dependencies] base64.workspace = true -hound.workspace = true +hound = {workspace = true, optional = true} js-sys.workspace = true leptos = {workspace = true, features = ["csr"]} leptos_router.workspace = true @@ -27,13 +27,15 @@ unicode-segmentation.workspace = true urlencoding.workspace = true wasm-bindgen.workspace = true web-sys = {workspace = true, features = ["CustomElementRegistry", "Window"]} -uiua = {path = "../..", default-features = false, features = [ - "batteries", - "web", -]} +uiua = {path = "../..", default-features = false} image = "0.25" leptos-use = {version = "0.13.0", default-features = false, features = [ "use_resize_observer", ]} wasm-bindgen-futures = "0.4.43" futures = "0.3.30" + +[features] +image = ["dep:image", "uiua/image"] +audio_encode = ["dep:hound", "uiua/audio_encode"] +default = ["uiua/batteries", "uiua/web", "image", "audio_encode"] diff --git a/pad/editor/src/backend.rs b/pad/editor/src/backend.rs index 8f525736e..63374b838 100644 --- a/pad/editor/src/backend.rs +++ b/pad/editor/src/backend.rs @@ -42,7 +42,8 @@ thread_local! { [ ("example.ua", EXAMPLE_UA), ("example.txt", EXAMPLE_TXT), - ("primitives.json", include_str!("../../../site/primitives.json")) + #[cfg(feature="default")] + ("primitives.json", include_str!("../../../site/primitives.json")), ] .map(|(path, content)| (PathBuf::from(path), content.as_bytes().to_vec())) .into(), @@ -214,6 +215,7 @@ impl SysBackend for WebBackend { .prompt_with_message("Enter a line of text for stdin") .unwrap_or(None)) } + #[cfg(feature = "image")] fn show_image(&self, image: image::DynamicImage, label: Option<&str>) -> Result<(), String> { let mut bytes = Cursor::new(Vec::new()); image @@ -383,6 +385,7 @@ impl SysBackend for WebBackend { (self.stdout.lock().unwrap()).push(OutputItem::Audio(wav_bytes, label.map(Into::into))); Ok(()) } + #[cfg(feature = "audio_encode")] fn stream_audio(&self, mut f: uiua::AudioStreamFn) -> Result<(), String> { let mut samples = Vec::new(); const SAMPLE_RATE: u32 = 44100; diff --git a/site/Cargo.toml b/site/Cargo.toml index cb67c4e3e..62a7ccfc6 100644 --- a/site/Cargo.toml +++ b/site/Cargo.toml @@ -28,8 +28,11 @@ wasm-bindgen.workspace = true web-sys = {workspace = true, features = ["HtmlElement"]} comrak = "0.39.0" console_error_panic_hook = "0.1.7" -uiua = {path = "..", default-features = false, features = ["batteries", "web"]} -uiua-editor = {path = "../pad/editor"} +uiua = {path = "..", default-features = false} +uiua-editor = {path = "../pad/editor", default_features = false} [features] -audio = [] # Dummy for rust-analyzer +# Dummy for rust-analyzer +audio = [] +gen_blog_html = [] +default = ["uiua/batteries", "uiua/web", "uiua-editor/default"] diff --git a/site/primitives.json b/site/primitives.json deleted file mode 100644 index 7ae33e69e..000000000 --- a/site/primitives.json +++ /dev/null @@ -1,1532 +0,0 @@ -{ - "&ap": { - "args": 1, - "outputs": 0, - "class": "Media", - "description": "Play some audio" - }, - "&apngs": { - "args": 2, - "outputs": 0, - "class": "Media", - "description": "Show an APNG" - }, - "&args": { - "args": 0, - "outputs": 1, - "class": "Env", - "description": "Get the command line arguments" - }, - "&asr": { - "args": 0, - "outputs": 1, - "class": "Media", - "description": "Get the sample rate of the audio output backend" - }, - "&ast": { - "args": 0, - "outputs": 0, - "modifier_args": 1, - "class": "Media", - "description": "Synthesize and stream audio" - }, - "&b": { - "args": 0, - "outputs": 0, - "class": "Misc", - "description": "Pause the execution and print the stack", - "experimental": true - }, - "&camcap": { - "args": 1, - "outputs": 1, - "class": "Misc", - "description": "Capture an image from a webcam" - }, - "&cd": { - "args": 1, - "outputs": 0, - "class": "Filesystem", - "description": "Change the current directory" - }, - "&cl": { - "args": 1, - "outputs": 0, - "class": "Stream", - "description": "Close a stream by its handle" - }, - "&clip": { - "args": 0, - "outputs": 1, - "class": "Misc", - "description": "Get the contents of the clipboard" - }, - "&ep": { - "args": 1, - "outputs": 0, - "class": "StdIO", - "description": "Print a value to stderr followed by a newline" - }, - "&epf": { - "args": 1, - "outputs": 0, - "class": "StdIO", - "description": "Print a value to stderr" - }, - "&exit": { - "args": 1, - "outputs": 0, - "class": "Misc", - "description": "Exit the program with a status code" - }, - "&fc": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Create a file and return a handle to it" - }, - "&fde": { - "args": 1, - "outputs": 0, - "class": "Filesystem", - "description": "Delete a file or directory" - }, - "&fe": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Check if a file, directory, or symlink exists at a path" - }, - "&ffi": { - "args": 2, - "outputs": 1, - "class": "Ffi", - "description": "Call a foreign function interface", - "experimental": true - }, - "&fif": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Check if a path is a file" - }, - "&fld": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "List the contents of a directory" - }, - "&fmd": { - "args": 1, - "outputs": 0, - "class": "Filesystem", - "description": "Create a directory" - }, - "&fo": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Open a file and return a handle to it" - }, - "&frab": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Read all the contents of a file into a byte array" - }, - "&fras": { - "args": 1, - "outputs": 1, - "class": "Filesystem", - "description": "Read all the contents of a file into a string" - }, - "&ftr": { - "args": 1, - "outputs": 0, - "class": "Filesystem", - "description": "Move a file or directory to the trash" - }, - "&fwa": { - "args": 2, - "outputs": 0, - "class": "Filesystem", - "description": "Write the entire contents of an array to a file" - }, - "&gifs": { - "args": 2, - "outputs": 0, - "class": "Media", - "description": "Show a gif" - }, - "&ims": { - "args": 1, - "outputs": 0, - "class": "Media", - "description": "Show an image" - }, - "&invk": { - "args": 1, - "outputs": 1, - "class": "Command", - "description": "Invoke a path with the system's default program" - }, - "&memcpy": { - "args": 2, - "outputs": 1, - "class": "Ffi", - "description": "Copy data from a pointer into an array", - "experimental": true - }, - "&memfree": { - "args": 1, - "outputs": 0, - "class": "Ffi", - "description": "Free a pointer", - "experimental": true - }, - "&memset": { - "args": 3, - "outputs": 0, - "class": "Ffi", - "description": "Write data from an array into a pointer", - "experimental": true - }, - "&p": { - "args": 1, - "outputs": 0, - "class": "StdIO", - "description": "Print a value to stdout followed by a newline" - }, - "&pf": { - "args": 1, - "outputs": 0, - "class": "StdIO", - "description": "Print a value to stdout" - }, - "&raw": { - "args": 1, - "outputs": 0, - "class": "Env", - "description": "Set the terminal to raw mode" - }, - "&rb": { - "args": 2, - "outputs": 1, - "class": "Stream", - "description": "Read at most n bytes from a stream" - }, - "&rl": { - "args": 1, - "outputs": 1, - "modifier_args": 1, - "class": "Stream", - "description": "Read lines from a stream" - }, - "&rs": { - "args": 2, - "outputs": 1, - "class": "Stream", - "description": "Read characters formed by at most n bytes from a stream" - }, - "&ru": { - "args": 2, - "outputs": 1, - "class": "Stream", - "description": "Read from a stream until a delimiter is reached" - }, - "&runc": { - "args": 1, - "outputs": 3, - "class": "Command", - "description": "Run a command and wait for it to finish" - }, - "&runi": { - "args": 1, - "outputs": 1, - "class": "Command", - "description": "Run a command and wait for it to finish" - }, - "&runs": { - "args": 1, - "outputs": 3, - "class": "Command", - "description": "Run a command with streaming IO" - }, - "&s": { - "args": 1, - "outputs": 0, - "class": "StdIO", - "description": "Print a nicely formatted representation of a value to stdout" - }, - "&sc": { - "args": 0, - "outputs": 1, - "class": "StdIO", - "description": "Read a line from stdin" - }, - "&seek": { - "args": 2, - "outputs": 0, - "class": "Stream", - "description": "Move to an absolute position in a file stream" - }, - "&sl": { - "args": 1, - "outputs": 0, - "class": "Misc", - "description": "Sleep for n seconds" - }, - "&tcpa": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Accept a connection with a TCP or TLS listener" - }, - "&tcpaddr": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Get the connection address of a TCP socket" - }, - "&tcpc": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Create a TCP socket and connect it to an address" - }, - "&tcpl": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Create a TCP listener and bind it to an address" - }, - "&tcpsnb": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Set a TCP socket to non-blocking mode" - }, - "&tcpsrt": { - "args": 2, - "outputs": 0, - "class": "Tcp", - "description": "Set the read timeout of a TCP socket in seconds" - }, - "&tcpswt": { - "args": 2, - "outputs": 0, - "class": "Tcp", - "description": "Set the write timeout of a TCP socket in seconds" - }, - "&tlsc": { - "args": 1, - "outputs": 1, - "class": "Tcp", - "description": "Create a TCP socket with TLS support" - }, - "&tlsl": { - "args": 3, - "outputs": 1, - "class": "Tcp", - "description": "Create a TLS listener and bind it to an address" - }, - "&ts": { - "args": 0, - "outputs": 1, - "class": "Env", - "description": "Get the size of the terminal" - }, - "&udpb": { - "args": 1, - "outputs": 1, - "class": "Udp", - "description": "Bind a UDP socket" - }, - "&udpr": { - "args": 1, - "outputs": 2, - "class": "Udp", - "description": "Receive a single message from a UDP socket" - }, - "&udps": { - "args": 3, - "outputs": 0, - "class": "Udp", - "description": "Send a message on a UDP socket" - }, - "&udpsml": { - "args": 2, - "outputs": 0, - "class": "Udp", - "description": "Set the maximum message length for incoming messages" - }, - "&var": { - "args": 1, - "outputs": 1, - "class": "Env", - "description": "Get the value of an environment variable" - }, - "&w": { - "args": 2, - "outputs": 0, - "class": "Stream", - "description": "Write an array to a stream" - }, - "above": { - "glyph": "◠", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Keep all arguments to a function above the outputs on the stack", - "experimental": true - }, - "absolute value": { - "glyph": "⌵", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Get the absolute value of a number" - }, - "add": { - "glyph": "+", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Add values" - }, - "anti": { - "glyph": "⌝", - "outputs": 1, - "modifier_args": 1, - "class": "InversionModifier", - "description": "Invert the behavior of a function, treating its first argument as a constant" - }, - "apng": { - "args": 2, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array of image frames in an APNG-encoded byte array" - }, - "arch": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the current architecture" - }, - "assert": { - "glyph": "⍤", - "args": 2, - "outputs": 0, - "class": "Misc", - "description": "Throw an error if a condition is not met" - }, - "atangent": { - "glyph": "∠", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Take the arctangent of two numbers" - }, - "audio": { - "args": 3, - "outputs": 1, - "class": "Encoding", - "description": "Encode audio into a byte array" - }, - "backward": { - "glyph": "˜", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Call a function with its arguments swapped" - }, - "base": { - "glyph": "⊥", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Get the base digits of a number" - }, - "below": { - "glyph": "◡", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Keep all arguments to a function below the outputs on the stack" - }, - "binary": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array into a compact binary representation", - "experimental": true - }, - "bits": { - "glyph": "⋯", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Encode an array as bits (LSB-first)" - }, - "both": { - "glyph": "∩", - "outputs": 1, - "modifier_args": 1, - "class": "Planet", - "description": "Call a function on two sets of values" - }, - "box": { - "glyph": "□", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Turn an array into a box" - }, - "bracket": { - "glyph": "⊓", - "outputs": 1, - "modifier_args": 2, - "class": "Planet", - "description": "Call two functions on two distinct sets of values" - }, - "by": { - "glyph": "⊸", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Duplicate a function's last argument before calling it" - }, - "bytes": { - "args": 2, - "outputs": 1, - "class": "Encoding", - "description": "Convert a value to a byte representation", - "experimental": true - }, - "case": { - "glyph": "⍩", - "outputs": 1, - "modifier_args": 1, - "class": "Misc", - "description": "Call a pattern matching case" - }, - "ceiling": { - "glyph": "⌈", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Round to the nearest integer towards ∞" - }, - "classify": { - "glyph": "⊛", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Assign a unique index to each unique row in an array" - }, - "complex": { - "glyph": "ℂ", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Make a complex number" - }, - "comptime": { - "outputs": 1, - "modifier_args": 1, - "class": "Comptime", - "description": "Run a function at compile time" - }, - "content": { - "glyph": "◇", - "outputs": 1, - "modifier_args": 1, - "class": "OtherModifier", - "description": "Unbox the arguments to a function before calling it" - }, - "couple": { - "glyph": "⊟", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Combine two arrays as rows of a new array" - }, - "csv": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array into a CSV string" - }, - "datetime": { - "args": 1, - "outputs": 1, - "class": "Time", - "description": "Get the date and time information from a time" - }, - "deduplicate": { - "glyph": "◴", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Remove duplicate rows from an array" - }, - "derivative": { - "glyph": "∂", - "outputs": 1, - "modifier_args": 1, - "class": "Algorithm", - "description": "Calculate the derivative of a mathematical expression", - "experimental": true - }, - "deshape": { - "glyph": "♭", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Make an array 1-dimensional" - }, - "dip": { - "glyph": "⊙", - "outputs": 1, - "modifier_args": 1, - "class": "Planet", - "description": "Temporarily pop the top value off the stack and call a function" - }, - "divide": { - "ascii": "%", - "glyph": "÷", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Divide values" - }, - "dllext": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the DLL extension for the current platform" - }, - "do": { - "glyph": "⍢", - "outputs": 1, - "modifier_args": 2, - "class": "IteratingModifier", - "description": "Repeat a function while a condition holds" - }, - "drop": { - "glyph": "↘", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Drop the first n rows of an array" - }, - "dump": { - "args": 0, - "outputs": 0, - "modifier_args": 1, - "class": "Debug", - "description": "Preprocess and print all stack values without popping them" - }, - "duplicate": { - "glyph": ".", - "args": 1, - "outputs": 2, - "class": "Stack", - "description": "Duplicate the top value on the stack" - }, - "each": { - "glyph": "∵", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Apply a function to each element of an array or arrays", - "deprecated": true - }, - "equals": { - "ascii": "=", - "glyph": "=", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for equality" - }, - "eta": { - "glyph": "η", - "args": 0, - "outputs": 1, - "class": "Constant", - "description": "The number of radians in a quarter circle" - }, - "evert": { - "glyph": "⧋", - "outputs": 1, - "modifier_args": 1, - "class": "OtherModifier", - "description": "Call a function with its arguments' axes reversed", - "experimental": true - }, - "exeext": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the executable extension for the current platform" - }, - "exponential": { - "glyph": "ₑ", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Get the exponential of a number" - }, - "fall": { - "glyph": "⍖", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the indices into an array if it were sorted descending" - }, - "fft": { - "args": 1, - "outputs": 1, - "class": "Algorithm", - "description": "Run the Fast Fourier Transform on an array" - }, - "fill": { - "glyph": "⬚", - "outputs": 1, - "modifier_args": 2, - "class": "OtherModifier", - "description": "Set the fill value for a function" - }, - "find": { - "glyph": "⌕", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Find the occurrences of one array in another" - }, - "first": { - "glyph": "⊢", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the first row of an array" - }, - "fix": { - "glyph": "¤", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Add a length-1 axis to an array" - }, - "flip": { - "ascii": ":", - "glyph": ":", - "args": 2, - "outputs": 2, - "class": "Stack", - "description": "Swap the top two values on the stack" - }, - "floor": { - "glyph": "⌊", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Round to the nearest integer towards ¯∞" - }, - "fold": { - "glyph": "∧", - "outputs": 1, - "modifier_args": 1, - "class": "AggregatingModifier", - "description": "Apply a function to aggregate arrays" - }, - "fork": { - "glyph": "⊃", - "outputs": 1, - "modifier_args": 2, - "class": "Planet", - "description": "Call two functions on the same values" - }, - "gap": { - "glyph": "⋅", - "outputs": 1, - "modifier_args": 1, - "class": "Planet", - "description": "Discard the top stack value then call a function" - }, - "gen": { - "args": 2, - "outputs": 1, - "class": "RNG", - "description": "Generate an array of random numbers with a seed" - }, - "geometric": { - "glyph": "⩜", - "outputs": 1, - "modifier_args": 1, - "class": "Algorithm", - "description": "Convert an operation to be in geometric algebra", - "experimental": true - }, - "get": { - "args": 2, - "outputs": 1, - "class": "Map", - "description": "Get the value corresponding to a key in a map array" - }, - "gif": { - "args": 2, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array of image frames in a GIF-encoded byte array" - }, - "graphemes": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Convert a string to a list of UTF-8 grapheme clusters" - }, - "greater or equal": { - "ascii": ">=", - "glyph": "≥", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for greater than or equal" - }, - "greater than": { - "glyph": ">", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for greater than" - }, - "group": { - "glyph": "⊕", - "args": 2, - "outputs": 1, - "modifier_args": 1, - "class": "AggregatingModifier", - "description": "Group elements of an array into buckets by index" - }, - "has": { - "args": 2, - "outputs": 1, - "class": "Map", - "description": "Check if a map array has a key" - }, - "hsv": { - "args": 1, - "outputs": 1, - "class": "Algorithm", - "description": "Convert a color array from RGB to HSV" - }, - "identity": { - "glyph": "∘", - "args": 1, - "outputs": 1, - "class": "Planet", - "description": "Do nothing with one value" - }, - "img": { - "args": 2, - "outputs": 1, - "class": "Encoding", - "description": "Encode an image into a byte array with the specified format" - }, - "indexof": { - "glyph": "⊗", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Find the first index of each row of one array in another" - }, - "infinity": { - "glyph": "∞", - "args": 0, - "outputs": 1, - "class": "Constant", - "description": "The biggest number" - }, - "insert": { - "args": 3, - "outputs": 1, - "class": "Map", - "description": "Insert a key-value pair into a map array" - }, - "integral": { - "glyph": "∫", - "outputs": 1, - "modifier_args": 1, - "class": "Algorithm", - "description": "Calculate an antiderivative of a mathematical expression", - "experimental": true - }, - "inventory": { - "glyph": "⍚", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Apply a function to each unboxed row of an array and re-box the results" - }, - "join": { - "glyph": "⊂", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Append two arrays end-to-end" - }, - "json": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array into a JSON string" - }, - "keep": { - "glyph": "▽", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Discard or copy some rows of an array" - }, - "last": { - "glyph": "⊣", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the last row of an array" - }, - "layout": { - "args": 2, - "outputs": 1, - "class": "Encoding", - "description": "Render text into an image array", - "experimental": true - }, - "length": { - "glyph": "⧻", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the number of rows in an array" - }, - "less or equal": { - "ascii": "<=", - "glyph": "≤", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for less than or equal" - }, - "less than": { - "glyph": "<", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for less than" - }, - "logarithm": { - "glyph": "ₙ", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Get the based logarithm of a number", - "deprecated": true - }, - "map": { - "args": 2, - "outputs": 1, - "class": "Map", - "description": "Create a hashmap from a list of keys and list values" - }, - "mask": { - "glyph": "⦷", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Mask the occurrences of one array in another" - }, - "match": { - "glyph": "≍", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Check if two arrays are exactly the same" - }, - "maximum": { - "glyph": "↥", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Take the maximum of two arrays" - }, - "memberof": { - "glyph": "∊", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Check if each row of one array exists in another" - }, - "memo": { - "outputs": 1, - "modifier_args": 1, - "class": "OtherModifier", - "description": "Memoize a function" - }, - "minimum": { - "glyph": "↧", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Take the minimum of two arrays" - }, - "modulo": { - "glyph": "◿", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Modulo values" - }, - "multiply": { - "ascii": "*", - "glyph": "×", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Multiply values" - }, - "negate": { - "ascii": "`", - "glyph": "¯", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Negate a number" - }, - "not": { - "glyph": "¬", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Logical not" - }, - "not equals": { - "ascii": "!=", - "glyph": "≠", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Compare for inequality" - }, - "now": { - "args": 0, - "outputs": 1, - "class": "Time", - "description": "Get the current time in seconds" - }, - "numprocs": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the number of processors on the current system" - }, - "obverse": { - "glyph": "⌅", - "outputs": 1, - "modifier_args": 1, - "class": "InversionModifier", - "description": "Define the various inverses of a function" - }, - "occurrences": { - "glyph": "⧆", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Mark each row of an array with its occurrence count", - "experimental": true - }, - "off": { - "glyph": "⤚", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Call a function but keep its first argument under the outputs on the stack" - }, - "on": { - "glyph": "⟜", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Call a function but keep its first argument on the top of the stack" - }, - "or": { - "glyph": "∨", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Logical OR and greatest common divisor", - "experimental": true - }, - "orient": { - "glyph": "⤸", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Change the order of the axes of an array" - }, - "os": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the current operating system" - }, - "osfamily": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the current operating system family" - }, - "parse": { - "glyph": "⋕", - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Parse a string as a number" - }, - "partition": { - "glyph": "⊜", - "args": 2, - "outputs": 1, - "modifier_args": 1, - "class": "AggregatingModifier", - "description": "Group sequential sections of an array" - }, - "path": { - "outputs": 2, - "modifier_args": 2, - "class": "Algorithm", - "description": "Find the shortest path between two things" - }, - "pathsep": { - "args": 0, - "outputs": 1, - "class": "Environment", - "description": "Get the primary path separator for the current platform" - }, - "pi": { - "glyph": "π", - "args": 0, - "outputs": 1, - "class": "Constant", - "description": "The ratio of a circle's circumference to its diameter" - }, - "pick": { - "glyph": "⊡", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Index a row or elements from an array" - }, - "pool": { - "outputs": 1, - "modifier_args": 1, - "class": "Thread", - "description": "Spawn a thread in a thread pool" - }, - "pop": { - "glyph": "◌", - "args": 1, - "outputs": 0, - "class": "Stack", - "description": "Discard the top stack value" - }, - "power": { - "glyph": "ⁿ", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Raise a value to a power" - }, - "pretty": { - "args": 1, - "outputs": 1, - "class": "Misc", - "description": "Convert a value to its pretty-printed output representation" - }, - "progressive indexof": { - "glyph": "⊘", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Get sequential indices of each row of an array in another", - "experimental": true, - "deprecated": true - }, - "quote": { - "args": 0, - "outputs": 1, - "modifier_args": 1, - "class": "Comptime", - "description": "Convert a string into code at compile time", - "experimental": true - }, - "random": { - "glyph": "⚂", - "args": 0, - "outputs": 1, - "class": "RNG", - "description": "Generate a random number in the range [0, 1)" - }, - "range": { - "glyph": "⇡", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Make an array of all natural numbers less than a number" - }, - "reach": { - "glyph": "𝄐", - "outputs": 1, - "modifier_args": 1, - "class": "Planet", - "description": "Call a function on the first and third values on the stack", - "experimental": true - }, - "reciprocal": { - "glyph": "⨪", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Get the reciprocal of a number", - "experimental": true - }, - "recur": { - "outputs": 1, - "modifier_args": 3, - "class": "Algorithm", - "description": "Execute a recursive or tree algorithm", - "experimental": true - }, - "recv": { - "args": 1, - "outputs": 1, - "class": "Thread", - "description": "Receive a value from a thread" - }, - "reduce": { - "glyph": "/", - "outputs": 1, - "modifier_args": 1, - "class": "AggregatingModifier", - "description": "Apply a reducing function to an array" - }, - "regex": { - "args": 2, - "outputs": 1, - "class": "Algorithm", - "description": "Match a regex pattern" - }, - "remove": { - "args": 2, - "outputs": 1, - "class": "Map", - "description": "Remove the value corresponding to a key from a map array" - }, - "repeat": { - "glyph": "⍥", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Repeat a function a number of times" - }, - "repr": { - "args": 1, - "outputs": 1, - "class": "Misc", - "description": "Convert a value to its code representation" - }, - "rerank": { - "glyph": "☇", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Change the rank of an array's rows", - "deprecated": true - }, - "reshape": { - "glyph": "↯", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Change the shape of an array" - }, - "reverse": { - "glyph": "⇌", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Reverse the rows of an array" - }, - "rise": { - "glyph": "⍏", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the indices into an array if it were sorted ascending" - }, - "rotate": { - "glyph": "↻", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Rotate the elements of an array by n" - }, - "round": { - "glyph": "⁅", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Round to the nearest integer" - }, - "rows": { - "glyph": "≡", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Apply a function to each row of an array or arrays" - }, - "scan": { - "glyph": "\\", - "args": 1, - "outputs": 1, - "modifier_args": 1, - "class": "AggregatingModifier", - "description": "Reduce, but keep intermediate values" - }, - "select": { - "glyph": "⊏", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Select multiple rows from an array" - }, - "self": { - "glyph": "˙", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Call a function with the same array as all arguments" - }, - "send": { - "args": 2, - "outputs": 0, - "class": "Thread", - "description": "Send a value to a thread" - }, - "shape": { - "glyph": "△", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get the dimensions of an array" - }, - "sign": { - "glyph": "±", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Numerical sign (1, ¯1, or 0)" - }, - "sine": { - "glyph": "∿", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Get the sine of a number" - }, - "sort": { - "glyph": "⍆", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Sort an array" - }, - "spawn": { - "outputs": 1, - "modifier_args": 1, - "class": "Thread", - "description": "Spawn a thread" - }, - "sqrt": { - "glyph": "√", - "args": 1, - "outputs": 1, - "class": "MonadicPervasive", - "description": "Take the square root of a number" - }, - "stack": { - "glyph": "?", - "args": 0, - "outputs": 0, - "class": "Debug", - "description": "Debug print all stack values without popping them" - }, - "stencil": { - "glyph": "⧈", - "args": 2, - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Call a function on windows of an array" - }, - "subtract": { - "glyph": "-", - "args": 2, - "outputs": 1, - "class": "DyadicPervasive", - "description": "Subtract values" - }, - "switch": { - "glyph": "⨬", - "outputs": 1, - "modifier_args": 2, - "class": "OtherModifier", - "description": "Call the function at the given index" - }, - "table": { - "glyph": "⊞", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Apply a function to each combination of rows of some arrays" - }, - "tag": { - "args": 0, - "outputs": 1, - "class": "Misc", - "description": "Generate a unique tag", - "deprecated": true - }, - "take": { - "glyph": "↙", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "Take the first n rows of an array" - }, - "tau": { - "glyph": "τ", - "args": 0, - "outputs": 1, - "class": "Constant", - "description": "The ratio of a circle's circumference to its radius" - }, - "timezone": { - "args": 0, - "outputs": 1, - "class": "Time", - "description": "Get the local timezone offset" - }, - "transpose": { - "glyph": "⍉", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Rotate the shape of an array" - }, - "try": { - "glyph": "⍣", - "outputs": 1, - "modifier_args": 2, - "class": "Misc", - "description": "Call a function and catch errors" - }, - "tryrecv": { - "args": 1, - "outputs": 1, - "class": "Thread", - "description": "Try to receive a value from a thread" - }, - "tuples": { - "glyph": "⧅", - "outputs": 1, - "modifier_args": 1, - "class": "IteratingModifier", - "description": "Get permutations or combinations of an array" - }, - "type": { - "args": 1, - "outputs": 1, - "class": "Misc", - "description": "Check the type of an array" - }, - "un": { - "glyph": "°", - "outputs": 1, - "modifier_args": 1, - "class": "InversionModifier", - "description": "Invert the behavior of a function" - }, - "under": { - "glyph": "⍜", - "outputs": 1, - "modifier_args": 2, - "class": "InversionModifier", - "description": "Operate on a transformed array, then reverse the transformation" - }, - "unique": { - "glyph": "◰", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get a mask of first occurrences of items in an array" - }, - "utf₈": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Convert a string to UTF-8 bytes" - }, - "voxels": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Project a voxel array to an image", - "experimental": true - }, - "wait": { - "args": 1, - "outputs": 1, - "class": "Thread", - "description": "Wait for a thread to finish and push its results to the stack" - }, - "where": { - "glyph": "⊚", - "args": 1, - "outputs": 1, - "class": "MonadicArray", - "description": "Get indices where array values are not equal to zero" - }, - "windows": { - "glyph": "◫", - "args": 2, - "outputs": 1, - "class": "DyadicArray", - "description": "The n-wise windows of an array", - "deprecated": true - }, - "with": { - "glyph": "⤙", - "outputs": 1, - "modifier_args": 1, - "class": "Stack", - "description": "Call a function but keep its last argument on the top of the stack" - }, - "xlsx": { - "args": 1, - "outputs": 1, - "class": "Encoding", - "description": "Encode an array into XLSX bytes" - } -} \ No newline at end of file diff --git a/site/src/blog.rs b/site/src/blog.rs index 04bc8453b..8f18ecd37 100644 --- a/site/src/blog.rs +++ b/site/src/blog.rs @@ -108,36 +108,3 @@ fn BlogRssButton() -> impl IntoView { } } - -#[cfg(test)] -#[test] -fn gen_blog_html() { - use std::{borrow::Cow, fs}; - - let list = include_str!("../blog/list.txt"); - for line in list - .lines() - .filter(|line| !line.is_empty() && !line.starts_with('#')) - { - let (path, _) = line.split_once('(').unwrap_or_default(); - let md_path = format!("blog/{path}-text.md"); - let mut markdown = - fs::read_to_string(&md_path).unwrap_or_else(|e| panic!("{md_path}: {e}")); - let mut lines: Vec> = markdown.lines().map(Cow::Borrowed).collect(); - lines.insert( - 0, - Cow::Borrowed("[Uiua](https://uiua.org)\n\n[Blog Home](https://uiua.org/blog)"), - ); - lines.insert( - 3, - Cow::Owned(format!( - "\n\n**You can read this post with full editor \ - features [here](https://uiua.org/blog/{path}).**\n\n" - )), - ); - markdown = lines.join("\n"); - let html = markdown_html(&markdown); - let html_path = format!("blog/{path}-html.html"); - fs::write(html_path, html).unwrap(); - } -} diff --git a/site/src/lib.rs b/site/src/lib.rs new file mode 100644 index 000000000..0411648f4 --- /dev/null +++ b/site/src/lib.rs @@ -0,0 +1,745 @@ +#![allow(non_snake_case, clippy::empty_docs, clippy::mutable_key_type)] + +pub mod blog; +pub mod docs; +pub mod examples; +pub mod idioms; +pub mod markdown; +pub mod other; +pub mod other_tutorial; +pub mod primitive; +pub mod tutorial; +pub mod uiuisms; + +use std::{cell::Cell, sync::OnceLock, time::Duration}; + +use base64::engine::{general_purpose::URL_SAFE, Engine}; +use js_sys::Date; +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use rand::prelude::*; +use uiua::{now, ConstantDef, Primitive, SysOp}; +use uiua_editor::{ + binding_name_class, lang, + utils::{ + get_april_fools_setting, get_april_fools_time, its_called_weewuh, set_april_fools, + ChallengeDef, + }, + Editor, EditorMode, Prim, EDITOR_SHORTCUTS, +}; +use wasm_bindgen::JsCast; +use web_sys::{Element, HtmlAudioElement}; + +use crate::{blog::*, docs::*, other::*, tutorial::Tutorial, uiuisms::*}; + +static START_TIME: OnceLock = OnceLock::new(); + +#[component] +pub fn Site() -> impl IntoView { + use Primitive::*; + provide_meta_context(); + START_TIME.get_or_init(|| Date::now() / 1000.0); + + // Choose a subtitle + let subtitles_common = [ + "A tacit array programming language", + "An array-oriented tacit programming language", + "A programming language for point-free enjoyers", + "A programming language for variable dislikers", + ]; + let subtitles_rare = [ + view!("Check out ""The Array Cast""!").into_view(), + view!("Point-Free or Die").into_view(), + view! { +
+ "🌍🪐"" " + + "⋅⋅⊙⋅⋅" + "∘" + +
+ } + .into_view(), + "Abandon nominativity. Embrace relativity.".into_view(), + view!(
"🗄️🍴"
).into_view(), + "It's got um...I um...arrays".into_view(), + ]; + let mut visits = visits(); + let subtitle = if visits % 3 < 2 { + subtitles_common[(visits as f64 * 2.0 / 3.0).round() as usize % subtitles_common.len()] + .into_view() + } else { + subtitles_rare[visits / 3 % subtitles_rare.len()].clone() + }; + visits = visits.overflowing_add(1).0; + + // Change the favicon to favicon-crayon.ico every 10 visits + if visits % 10 == 0 { + let link = document().create_element("link").unwrap(); + link.set_attribute("rel", "icon").unwrap(); + link.set_attribute("href", "/favicon-crayon.ico").unwrap(); + document().head().unwrap().append_child(&link).unwrap(); + } + (window().local_storage().unwrap().unwrap()) + .set_item("visits", &visits.to_string()) + .unwrap(); + + let logo_src = match visits % 9 { + _ if its_called_weewuh() => "/assets/weewuh-logo.png", + 1 => "/assets/uiua-logo.png", + 3 => "/assets/uiua-logo-pride.png", + 5 => "/assets/uiua-logo-scrambledine.png", + 7 => "/assets/uiua-logo-jacob.svg", + _ if Date::new_0().get_month() == 5 => "/assets/uiua-logo-pride.png", + _ => "/assets/uiua-logo.png", + }; + + let toggle_april_fools_colors = move |_| { + set_april_fools(!get_april_fools_setting()); + _ = window().location().reload(); + }; + + view! { + + + + + + +
+ + +
+ + }> + + + + + + + + + + + + +
+
+ } +} + +fn visits() -> usize { + (window().local_storage().unwrap().unwrap()) + .get_item("visits") + .ok() + .flatten() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) +} + +fn weewuh() { + let i = (now() % 1.0 * 100.0) as u32; + let src = match i { + 0 => "/assets/ooh-ee-ooh-ah.mp3", + 1..=4 => "/assets/wee-wah.mp3", + _ => "/assets/wee-wuh.mp3", + }; + if let Ok(audio) = HtmlAudioElement::new_with_src(src) { + _ = audio.play(); + } +} + +#[component] +pub fn MainPage() -> impl IntoView { + use Primitive::*; + + let visits = visits(); + + fn rich_prim(prim: Primitive, text: &'static str, example: &'static str) -> impl Fn() -> View { + move || { + view! { +

" "{text}":"

+ + } + .into_view() + } + } + + let mut rich_prims = vec![ + rich_prim( + Select, + "for re-sequencing array items", + r#"⊏ 2_1_3_0_4 "loco!""#, + ), + rich_prim( + Deduplicate, + "for removing duplicate items", + r#"◴ "hello, world!""#, + ), + rich_prim(Sort, "for... sorting", "⍆ [2 8 3 2 1 5]"), + rich_prim( + Keep, + "for filtering", + r#"▽ [1 1 1 0 0 0 0 1 0] "filter me""#, + ), + rich_prim( + Where, + "for finding the indices of things", + "⊚≤5 [4 8 3 9 2 7 1]", + ), + rich_prim(Mask, "for finding subsequences", r#"⦷ "ra" "abracadabra""#), + rich_prim( + Partition, + "for splitting arrays by sequential keys", + r#"⬚@ ⊜∘⊸≠@ "Oh boy, neat!""#, + ), + rich_prim( + Un, + "for inverting the behavior of a function", + "°(÷2+1) [1 2 3 4]", + ), + rich_prim( + Under, + "for modifying only part of an array (among other things)", + r#"⍜(↙2|×10) 1_2_3_4_5"#, + ), + ]; + + let indices = if visits < 4 { + vec![0, rich_prims.len() - 3, rich_prims.len() - 1] + } else { + let mut rng = SmallRng::seed_from_u64(visits as u64); + let mut indices: Vec = (0..rich_prims.len()).collect(); + indices.shuffle(&mut rng); + indices.truncate(3); + indices.sort_unstable(); + indices + }; + let mut rich_prims: Vec<_> = indices + .into_iter() + .rev() + .map(|i| rich_prims.remove(i)()) + .collect(); + rich_prims.reverse(); + + view! { + + <div id="links"> + <div> + <A href="/install">"Installation"</A> + <A href="/docs">"Documentation"</A> + <A href="/tour">"Language Tour"</A> + </div> + <div> + <A href="/tutorial/introduction" class="slow-pulse">"Tutorial"</A> + <A href="/pad">"Pad"</A> + <A href="/blog">"Blog"</A> + <a href="https://discord.gg/3r9nrfYhCc">"Discord"</a> + <a href="https://github.com/uiua-lang/uiua">"GitHub"</a> + </div> + </div> + <Editor + mode=EditorMode::Showcase + examples=examples::EXAMPLES + .iter() + .map(|&ex| + if its_called_weewuh() { + match ex { + examples::UIUA => examples::WEEWUH, + examples::LOGO => examples::WEEWUH_LOGO, + examples::PALINDROME => examples::WEEWUH_PALINDROME, + _ => ex, + } + } else{ + ex + } + ) + .map(ToString::to_string) + .collect() + help={&[ + "Type a glyph's name, then run to format the names into glyphs.", + "You can run with ctrl/shift + enter.", + ]}/> + <br/> + <p class="main-text">{lang}" "<span class="wee-wuh-span">"("<i>"wee-wuh "</i><button on:click=|_| weewuh() class="sound-button">"🔉"</button>")"</span>" is a general purpose array-oriented programming language with a focus on simplicity, beauty, and "<a href="https://en.wikipedia.org/wiki/Tacit_programming">"tacit"</a>" code."</p> + <p class="main-text">{lang}" lets you write code that is as short as possible while remaining readable, so you can focus on problems rather than ceremony."</p> + <p class="main-text">"The language is not yet stable, as its design space is still being explored. However, it is already quite powerful and fun to use!"</p> + <div class="features"> + <div> + <div> + <Hd id="saying less">"Saying Less"</Hd> + <p>{lang}" combines the array-oriented programming paradigm with a stack-based execution model. Combining these already terse systems results in code with a very high information density and little syntactic noise."</p> + <Editor example="⍥◡+9 .1"/> + <p>"If this code seems weird and unreadable, that's okay! It's important to remember that "<a href="https://vector-of-bool.github.io/2018/10/31/become-perl.html">"foreign ≠ confusing"</a>"."</p> + </div> + <div> + <Hd id="true-arrays">"True Arrays"</Hd> + <p>{lang}"'s one and only composite data type, the array, is based on those of APL, J, and BQN. They are multidimensional and rank-polymorphic, meaning that an operation that applies to one item also applies to many items."</p> + <Editor example="◿5 ↯3_4 ⇡12"/> + </div> + <div> + <Hd id="rich-primitives">"Rich Primitives"</Hd> + <p>{lang}" has lots of built-in functions for all your array manipulation needs. Just a few examples:"</p> + { rich_prims } + </div> + <div> + <Hd id="syntactic-simplicity">"Syntactic Simplicity"</Hd> + <p>{lang}" has a simple, context-free, LL(3) grammar. Code runs from "<A href="/rtl">"right to left"</A>", top to bottom, with only "<A href="/tutorial/functions#modifiers">"one precedence rule"</A>". As operators are to the left of their operands, "{lang}" code reads a little bit like a Lisp, but with fewer parentheses."</p> + </div> + <div> + <Hd id="system-apis">"System APIs"</Hd> + <p>{lang}" has functions for spawning threads, interacting with the file system, communicating over network sockets, and "<A href="/docs/system">"more"</A>"."</p> + </div> + <div> + <Hd id="rust-ingegration">"Rust Integration"</Hd> + <p>{lang}" can be embedded in Rust programs "<a href="https://docs.rs/uiua">"as a library"</a>"."</p> + </div> + <div> + <Hd id="ffi">"FFI"</Hd> + <p>{lang}" has experimental support for calling functions from shared libraries through "<Prim prim=Sys(SysOp::Ffi)/>"."</p> + </div> + </div> + <div> + <div> + <Hd id="friendly-glyphs">"Friendly Glyphs"</Hd> + <p>{lang}" uses special characters for built-in functions that remind you what they do!"</p> + <Editor example="⚂ # Random number"/> + <Editor example="⇡8 # Range up to"/> + <Editor example="⇌ 1_2_3_4 # Reverse"/> + <Editor example="⌕ \"ab\" \"abracabra\" # Find"/> + <Editor example="⊟ 1_2_3 4_5_6 # Couple"/> + <p>"Unlike other array languages, "{lang}" does not have monadic and dyadic versions of each glyph. Every glyph does only one thing, so you don't need to parse an entire expression to know which version it is."</p> + </div> + <div> + <Hd id="unicode-formatter">"Unicode Formatter"</Hd> + <p>{lang}" has the terseness and expressivity afforded by Unicode glyphs without the need for special keyboard or editor support. Instead, the language comes with a formatter that converts the names of built-in functions into glyphs."</p> + <Editor example="floor*10repeatrand5" help={&["", "Click to format ⇡⇡⇡ "]}/> + </div> + <div> + <Hd id="multimedia-output">"Multimedia Output"</Hd> + <p>{lang}" has built-in facilities for generating images and audio. Just make arrays of the pixel data or audio samples. You can even make GIFs!"</p> + { + if cfg!(debug_assertions) { + None + } else { + Some(view!{ + <Editor example="⍉⊞<⊞+⇡3∿∩(÷25)⇡240⇡80"/> + <Editor example="÷3/+∿⊞×⊟×1.5.220×τ÷⟜⇡&asr"/> + <Editor example="Xy ← ⍉⍉⊞⊟.÷⟜⇡\nF ← ⍉◿1⊂⊃(+/÷|÷3+1∿×τ+)Xy\n≡F100 ÷⟜⇡10"/> + }) + } + } + <p>"The "{lang}" logo was made with "{lang}"! Check example 5 at the top of the page."</p> + </div> + <div> + <Hd id="language-server">"Language Server"</Hd> + <p>"The "{lang}" interpreter has a built-in language server that uses the "<a href="https://microsoft.github.io/language-server-protocol/">"Language Server Protocol"</a>", so you can "<A href="/install#editor-support">"use it with your favorite editor"</A>"."</p> + </div> + </div> + </div> + <div> + <Hd id="getting-started">"Getting Started"</Hd> + <p>"For more examples of what "{lang}" code looks like and what it can do, see the examples in the editor at the top of this page."</p> + <p>"For a quick overview of how the language works, see the "<A href="/tour">"Language Tour"</A>"."</p> + <p>"For a full tutorial, see the "<A href="/docs#tutorial">"Tutorial"</A>"."</p> + <p>"For a reference of all the built-in functions, the documentation has a "<A href="/docs#functions">"full list"</A>"."</p> + </div> + } +} + +#[component] +fn NotFound() -> impl IntoView { + view! { + <h1>"Page not found"</h1> + <Editor example="$ Where could it be?\n×101⧻⊜⧻≠@ ."/> + <h3><A href="/">"Go home"</A></h3> + } +} + +#[cfg(feature = "gen_blog_html")] +fn prim_html(prim: Primitive, glyph_only: bool, hide_docs: bool) -> String { + use uiua::PrimDoc; + + let symbol_class = format!("prim-glyph {}", uiua_editor::prim_class(prim)); + let symbol = prim.to_string(); + let name = if !glyph_only && symbol != prim.name() { + format!(" {}", prim.name()) + } else { + "".to_string() + }; + let href = format!("/docs/{}", prim.name()); + let mut title = String::new(); + if let Some(ascii) = prim.ascii() { + title.push_str(&format!("({ascii})")); + } + if prim.glyph().is_some() && glyph_only { + if !title.is_empty() { + title.push(' '); + } + title.push_str(prim.name()); + } + if let Primitive::Sys(op) = prim { + title.push_str(op.long_name()); + title.push(':'); + title.push('\n'); + } + if !hide_docs { + let doc = PrimDoc::from(prim); + if glyph_only && !title.is_empty() && !matches!(prim, Primitive::Sys(_)) { + title.push_str(": "); + } + title.push_str(&doc.short_text()); + } + if title.is_empty() { + format!( + r#"<a href="{href}"class="prim-code-a"> + <code><span class="{symbol_class}">{symbol}</span>{name}</code> + </a>"#, + ) + } else { + format!( + r#"<a href="{href}" class="prim-code-a"> + <code class="prim-code" data-title="{title}"><span class="{symbol_class}">{symbol}</span>{name}</code> + </a>"#, + ) + } +} + +#[component] +pub fn Prims<const N: usize>( + prims: [Primitive; N], + #[prop(optional)] show_names: bool, +) -> impl IntoView { + prims + .into_iter() + .map(|prim| view!(<Prim prim=prim glyph_only={!show_names}/>)) + .collect::<Vec<_>>() +} + +#[component] +#[allow(clippy::needless_lifetimes)] +fn Const<'a>(con: &'a ConstantDef) -> impl IntoView { + let name_class = binding_name_class(con.name); + let outer_class = if name_class.is_some() { + "prim-code-a code-background" + } else { + "prim-code-a" + }; + let inner_class = format!( + "prim-code stack-function {}", + name_class.unwrap_or_default() + ); + view! { + <a href={format!("/docs/constants#{}", con.name)} class=outer_class> + <code + id={con.name} + class=inner_class + data-title={ con.doc().lines().next().unwrap_or_default().to_string() } + >{ con.name }</code> + </a> + } +} + +#[track_caller] +#[allow(clippy::manual_map)] +fn get_element<T: JsCast>(id: &str) -> Option<T> { + if let Some(elem) = document().get_element_by_id(id) { + Some(elem.dyn_into().unwrap()) + } else { + None + } +} + +#[track_caller] +pub fn element<T: JsCast>(id: &str) -> T { + if let Some(elem) = get_element(id) { + elem + } else { + panic!("#{id} not found") + } +} + +#[component] +pub fn Tour() -> impl IntoView { + title_markdown("Language Tour", "/text/tour.md", View::default()) +} + +#[component] +pub fn PadPage() -> impl IntoView { + let src = pad_src(); + let version = format!("{} {}", lang(), uiua::VERSION); + let help = &[ + match lang() { + "Weewuh" => "Note: Weewuh is not yet stable", + _ => "Note: Uiua is not yet stable", + }, + &version, + ]; + view! { + <Title text=format!("Pad - {}", lang())/> + <Editor mode=EditorMode::Pad example={ &src } help=help/> + <br/> + <br/> + { + let date = Date::new_0(); + if date.get_month() == 11 || date.get_month() == 10 && date.get_date() > 15 { + let year = date.get_full_year(); + const CODE: &str = "6680-a10280cf"; + let copy_code = move |_| { + _ = window().navigator().clipboard().write_text(CODE) + }; + Some(view! { + <p>"Join the official "<a href={format!("https://adventofcode.com/{year}")}>"Advent of Code "{year}</a>" "{lang}" community leaderboard "<a href={format!("https://adventofcode.com/{year}/leaderboard/private")}>"here!"</a>"  "<button on:click=copy_code>"Copy code"</button></p> + }) + } else { + None + } + } + <br/> + <p>"You can load files into the pad by dragging and dropping them into the window."</p> + <p>"Replace "<code>"pad"</code>" in links with "<code>"embed"</code>" or "<code>"embedpad"</code>" to embed the editor."</p> + <p>"Keyboard shortcuts:"</p> + <code class="code-block"> + { EDITOR_SHORTCUTS } + </code> + <p>"Want a pad-like experience in the native interpreter? Try the "<code>"uiua -w"</code>" command to show output in a window."</p> + <p>"You can download the newest version of the native interpreter "<a href="https://github.com/uiua-lang/uiua/releases">"here"</a>"."</p> + } +} + +#[component] +pub fn EmbedPad() -> impl IntoView { + let src = pad_src(); + view! { + <Editor mode=EditorMode::Pad example={ &src }/> + } +} + +#[component] +pub fn Embed() -> impl IntoView { + let src = pad_src(); + view! { + <Editor mode=EditorMode::Example example={ &src }/> + } +} + +fn pad_src() -> String { + let mut src = use_query_map() + .with_untracked(|params| params.get("src").cloned()) + .unwrap_or_default(); + if let Some((_, encoded)) = src.split_once("__") { + // logging::log!("{:?}", encoded); + if let Ok(decoded) = URL_SAFE.decode(encoded.as_bytes()) { + src = String::from_utf8_lossy(&decoded).to_string(); + } + } else if let Ok(decoded) = URL_SAFE.decode(src.as_bytes()) { + src = String::from_utf8_lossy(&decoded).to_string(); + } + src +} + +#[test] +fn site() { + type Test = ( + std::path::PathBuf, + String, + std::thread::JoinHandle<(uiua::UiuaResult<uiua::Compiler>, bool)>, + ); + fn recurse_dir(path: &std::path::Path, threads: &mut Vec<Test>) -> std::io::Result<()> { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if entry.file_type()?.is_file() { + for line in std::fs::read_to_string(&path)?.lines() { + let (code, should_fail) = + if let Some(code) = line.trim().strip_prefix(r#"<Editor example=""#) { + if let Some(code) = code.strip_suffix(r#""/>"#) { + (code, false) + } else if let Some(code) = code.strip_suffix(r#""/> // Should fail"#) { + (code, true) + } else { + continue; + } + } else if let Some(line) = + line.strip_prefix(" { ").and_then(|line| { + (line.split(", ").nth(2)) + .and_then(|rest| rest.strip_prefix('"')) + .and_then(|rest| rest.strip_suffix("\") }")) + }) + { + (line, false) + } else { + continue; + }; + let code = code + .replace("\\\\n", "<escaped-newline>") + .replace("\\n", "\n") + .replace("\\\"", "\"") + .replace("\\\\", "\\") + .replace("<escaped-newline>", "\\n"); + if code.contains("\"git:") + || [uiua::SysOp::AudioPlay, uiua::SysOp::GifShow] + .iter() + .any(|p| code.contains(p.name())) + { + continue; + } + threads.push(( + path.to_path_buf(), + code.clone(), + std::thread::spawn(move || { + ( + uiua::Uiua::with_backend( + uiua_editor::backend::WebBackend::default(), + ) + .run_str(&code), + should_fail, + ) + }), + )); + } + } else if entry.file_type()?.is_dir() { + recurse_dir(&path, threads)?; + } + } + Ok(()) + } + let mut threads = Vec::new(); + recurse_dir("src".as_ref(), &mut threads).unwrap(); + assert!(threads.len() > 50); + for (path, code, thread) in threads { + match thread.join().unwrap() { + (Err(e), false) => { + panic!( + "Test failed in {}\n{}\n{}", + path.display(), + code, + e.report() + ); + } + (Err(_), true) => {} + (Ok(mut comp), should_fail) => { + if let Some(diag) = comp.take_diagnostics().into_iter().next() { + if !should_fail { + panic!( + "Test failed in {}\n{}\n{}", + path.display(), + code, + diag.report() + ); + } + continue; + } + if should_fail { + panic!("Test should have failed in {}\n{}", path.display(), code); + } + } + } + } +} + +#[component] +#[allow(clippy::needless_lifetimes)] +fn Hd<'a>(id: &'a str, #[prop(optional)] class: &'a str, children: Children) -> impl IntoView { + let id = id.to_string(); + view! { + <h2 id={id.clone()}> + <a class={format!("header {class}")} href={ format!("#{id}") }>{children()}</a> + </h2> + } +} + +#[component] +#[allow(clippy::needless_lifetimes)] +fn Hd3<'a>(id: &'a str, #[prop(optional)] class: &'a str, children: Children) -> impl IntoView { + let id = id.to_string(); + view! { + <h3 id={id.clone()}> + <a class={format!("header {class}")} href={ format!("#{id}") }>{children()}</a> + </h3> + } +} + +#[component] +fn ScrollToHash() -> impl IntoView { + move || { + let location = use_location(); + create_effect(move |_| { + let hash = location.hash.get(); + if !hash.is_empty() { + set_timeout( + move || { + let id = hash.trim_start_matches('#'); + if let Some(elem) = get_element::<Element>(id) { + elem.scroll_into_view(); + } + }, + Duration::from_millis(0), + ) + } + }); + } +} + +#[component] +pub fn Challenge<'a, P: IntoView + 'static>( + number: u8, + prompt: P, + example: &'a str, + answer: &'a str, + tests: &'a [&'a str], + hidden: &'a str, + #[prop(optional)] default: &'a str, + #[prop(optional)] flip: bool, + #[prop(optional)] best_answer: &'a str, +) -> impl IntoView { + let def = ChallengeDef { + example: example.into(), + intended_answer: answer.into(), + best_answer: (!best_answer.is_empty()).then(|| best_answer.into()), + tests: tests.iter().copied().map(Into::into).collect(), + hidden: hidden.into(), + flip, + did_init_run: Cell::new(false), + }; + view! { + <div class="challenge"> + <h3>"Challenge "{number}</h3> + <p>"Write a program that "<strong>{prompt}</strong>"."</p> + <Editor challenge=def example=default/> + </div> + } +} diff --git a/site/src/main.rs b/site/src/main.rs index 7d9c7b4e8..03d37d232 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -1,38 +1,5 @@ -#![allow(non_snake_case, clippy::empty_docs, clippy::mutable_key_type)] - -mod blog; -mod docs; -mod examples; -mod idioms; -mod markdown; -mod other; -mod other_tutorial; -mod primitive; -mod tutorial; -mod uiuisms; - -use std::{cell::Cell, sync::OnceLock, time::Duration}; - -use base64::engine::{general_purpose::URL_SAFE, Engine}; -use js_sys::Date; use leptos::*; -use leptos_meta::*; -use leptos_router::*; -use rand::prelude::*; -use uiua::{now, ConstantDef, Primitive, SysOp}; -use uiua_editor::{ - binding_name_class, lang, - utils::{ - get_april_fools_setting, get_april_fools_time, its_called_weewuh, set_april_fools, - ChallengeDef, - }, - Editor, EditorMode, Prim, EDITOR_SHORTCUTS, -}; -use wasm_bindgen::JsCast; -use web_sys::{Element, HtmlAudioElement}; - -use crate::{blog::*, docs::*, other::*, tutorial::Tutorial, uiuisms::*}; - +use site::*; pub fn main() { console_error_panic_hook::set_once(); @@ -44,763 +11,3 @@ pub fn main() { mount_to_body(|| view!( <Site/>)); } - -static START_TIME: OnceLock<f64> = OnceLock::new(); - -#[component] -pub fn Site() -> impl IntoView { - use Primitive::*; - provide_meta_context(); - START_TIME.get_or_init(|| Date::now() / 1000.0); - - // Choose a subtitle - let subtitles_common = [ - "A tacit array programming language", - "An array-oriented tacit programming language", - "A programming language for point-free enjoyers", - "A programming language for variable dislikers", - ]; - let subtitles_rare = [ - view!("Check out "<a href="https://arraycast.com/">"The Array Cast"</a>"!").into_view(), - view!(<a href="https://youtu.be/seVSlKazsNk">"Point-Free or Die"</a>).into_view(), - view! { - <div style="font-style: normal"> - <a href="/tutorial/morestack#planet-notation" style="text-decoration: none">"🌍🪐"</a>" " - <code style="font-style: normal"> - <span class="monadic-modifier">"⋅⋅⊙⋅⋅"</span> - <span class="stack-function">"∘"</span> - </code> - </div> - } - .into_view(), - "Abandon nominativity. Embrace relativity.".into_view(), - view!(<div style="font-style: normal"><Prim prim=Under glyph_only=true/>"🗄️🍴"</div>).into_view(), - "It's got um...I um...arrays".into_view(), - ]; - let mut visits = visits(); - let subtitle = if visits % 3 < 2 { - subtitles_common[(visits as f64 * 2.0 / 3.0).round() as usize % subtitles_common.len()] - .into_view() - } else { - subtitles_rare[visits / 3 % subtitles_rare.len()].clone() - }; - visits = visits.overflowing_add(1).0; - - // Change the favicon to favicon-crayon.ico every 10 visits - if visits % 10 == 0 { - let link = document().create_element("link").unwrap(); - link.set_attribute("rel", "icon").unwrap(); - link.set_attribute("href", "/favicon-crayon.ico").unwrap(); - document().head().unwrap().append_child(&link).unwrap(); - } - (window().local_storage().unwrap().unwrap()) - .set_item("visits", &visits.to_string()) - .unwrap(); - - let logo_src = match visits % 9 { - _ if its_called_weewuh() => "/assets/weewuh-logo.png", - 1 => "/assets/uiua-logo.png", - 3 => "/assets/uiua-logo-pride.png", - 5 => "/assets/uiua-logo-scrambledine.png", - 7 => "/assets/uiua-logo-jacob.svg", - _ if Date::new_0().get_month() == 5 => "/assets/uiua-logo-pride.png", - _ => "/assets/uiua-logo.png", - }; - - let toggle_april_fools_colors = move |_| { - set_april_fools(!get_april_fools_setting()); - _ = window().location().reload(); - }; - - view! { - <Router> - <ScrollToHash/> - <Routes> - <Route path="embedpad" view=EmbedPad/> - <Route path="embed" view=Embed/> - <Route path="*" view=move || view! { - <main> - <div id="top"> - <div id="header"> - <div id="header-left"> - <h1> - <A id="header-uiua" href="/"> - <img src=logo_src style="height: 1em" alt={format!("{} logo", lang())} /> - " "{lang} - </A> - </h1> - <p id="subtitle">{ subtitle.clone() }</p> - </div> - <div id="nav"> - { - if get_april_fools_time() { - Some(view!( - <div title="Enable April Fool's colors (refresh after changing)"> - "April Fool's:" - <input - type="checkbox" - checked=get_april_fools_setting - on:change=toggle_april_fools_colors - /> - </div> - )) - } else { - None - } - } - <a class="pls-no-block" href="https://github.com/sponsors/uiua-lang">"Support "{lang}"'s development"</a> - <a href="/">"Home"</a> - </div> - </div> - <Outlet/> - </div> - </main> - }> - <Route path="" view=MainPage/> - <Route path="tutorial/:page?" view=Tutorial/> - <Route path="docs/:page?" view=Docs/> - <Route path="isms/:search?" view=Uiuisms/> - <Route path="pad" view=PadPage/> - <Route path="install" view=Install/> - <Route path="tour" view=Tour/> - <Route path="isms" view=Uiuisms/> - <Route path="rtl" view=RightToLeft/> - <Route path="blog/:page?" view=Blog/> - <Route path="*" view=NotFound/> - </Route> - </Routes> - </Router> - } -} - -fn visits() -> usize { - (window().local_storage().unwrap().unwrap()) - .get_item("visits") - .ok() - .flatten() - .and_then(|s| s.parse().ok()) - .unwrap_or(0) -} - -fn weewuh() { - let i = (now() % 1.0 * 100.0) as u32; - let src = match i { - 0 => "/assets/ooh-ee-ooh-ah.mp3", - 1..=4 => "/assets/wee-wah.mp3", - _ => "/assets/wee-wuh.mp3", - }; - if let Ok(audio) = HtmlAudioElement::new_with_src(src) { - _ = audio.play(); - } -} - -#[component] -pub fn MainPage() -> impl IntoView { - use Primitive::*; - - let visits = visits(); - - fn rich_prim(prim: Primitive, text: &'static str, example: &'static str) -> impl Fn() -> View { - move || { - view! { - <p><Prim prim=prim/>" "{text}":"</p> - <Editor example=example/> - } - .into_view() - } - } - - let mut rich_prims = vec![ - rich_prim( - Select, - "for re-sequencing array items", - r#"⊏ 2_1_3_0_4 "loco!""#, - ), - rich_prim( - Deduplicate, - "for removing duplicate items", - r#"◴ "hello, world!""#, - ), - rich_prim(Sort, "for... sorting", "⍆ [2 8 3 2 1 5]"), - rich_prim( - Keep, - "for filtering", - r#"▽ [1 1 1 0 0 0 0 1 0] "filter me""#, - ), - rich_prim( - Where, - "for finding the indices of things", - "⊚≤5 [4 8 3 9 2 7 1]", - ), - rich_prim(Mask, "for finding subsequences", r#"⦷ "ra" "abracadabra""#), - rich_prim( - Partition, - "for splitting arrays by sequential keys", - r#"⬚@ ⊜∘⊸≠@ "Oh boy, neat!""#, - ), - rich_prim( - Un, - "for inverting the behavior of a function", - "°(÷2+1) [1 2 3 4]", - ), - rich_prim( - Under, - "for modifying only part of an array (among other things)", - r#"⍜(↙2|×10) 1_2_3_4_5"#, - ), - ]; - - let indices = if visits < 4 { - vec![0, rich_prims.len() - 3, rich_prims.len() - 1] - } else { - let mut rng = SmallRng::seed_from_u64(visits as u64); - let mut indices: Vec<usize> = (0..rich_prims.len()).collect(); - indices.shuffle(&mut rng); - indices.truncate(3); - indices.sort_unstable(); - indices - }; - let mut rich_prims: Vec<_> = indices - .into_iter() - .rev() - .map(|i| rich_prims.remove(i)()) - .collect(); - rich_prims.reverse(); - - view! { - <Title text=lang()/> - <div id="links"> - <div> - <A href="/install">"Installation"</A> - <A href="/docs">"Documentation"</A> - <A href="/tour">"Language Tour"</A> - </div> - <div> - <A href="/tutorial/introduction" class="slow-pulse">"Tutorial"</A> - <A href="/pad">"Pad"</A> - <A href="/blog">"Blog"</A> - <a href="https://discord.gg/3r9nrfYhCc">"Discord"</a> - <a href="https://github.com/uiua-lang/uiua">"GitHub"</a> - </div> - </div> - <Editor - mode=EditorMode::Showcase - examples=examples::EXAMPLES - .iter() - .map(|&ex| - if its_called_weewuh() { - match ex { - examples::UIUA => examples::WEEWUH, - examples::LOGO => examples::WEEWUH_LOGO, - examples::PALINDROME => examples::WEEWUH_PALINDROME, - _ => ex, - } - } else{ - ex - } - ) - .map(ToString::to_string) - .collect() - help={&[ - "Type a glyph's name, then run to format the names into glyphs.", - "You can run with ctrl/shift + enter.", - ]}/> - <br/> - <p class="main-text">{lang}" "<span class="wee-wuh-span">"("<i>"wee-wuh "</i><button on:click=|_| weewuh() class="sound-button">"🔉"</button>")"</span>" is a general purpose array-oriented programming language with a focus on simplicity, beauty, and "<a href="https://en.wikipedia.org/wiki/Tacit_programming">"tacit"</a>" code."</p> - <p class="main-text">{lang}" lets you write code that is as short as possible while remaining readable, so you can focus on problems rather than ceremony."</p> - <p class="main-text">"The language is not yet stable, as its design space is still being explored. However, it is already quite powerful and fun to use!"</p> - <div class="features"> - <div> - <div> - <Hd id="saying less">"Saying Less"</Hd> - <p>{lang}" combines the array-oriented programming paradigm with a stack-based execution model. Combining these already terse systems results in code with a very high information density and little syntactic noise."</p> - <Editor example="⍥◡+9 .1"/> - <p>"If this code seems weird and unreadable, that's okay! It's important to remember that "<a href="https://vector-of-bool.github.io/2018/10/31/become-perl.html">"foreign ≠ confusing"</a>"."</p> - </div> - <div> - <Hd id="true-arrays">"True Arrays"</Hd> - <p>{lang}"'s one and only composite data type, the array, is based on those of APL, J, and BQN. They are multidimensional and rank-polymorphic, meaning that an operation that applies to one item also applies to many items."</p> - <Editor example="◿5 ↯3_4 ⇡12"/> - </div> - <div> - <Hd id="rich-primitives">"Rich Primitives"</Hd> - <p>{lang}" has lots of built-in functions for all your array manipulation needs. Just a few examples:"</p> - { rich_prims } - </div> - <div> - <Hd id="syntactic-simplicity">"Syntactic Simplicity"</Hd> - <p>{lang}" has a simple, context-free, LL(3) grammar. Code runs from "<A href="/rtl">"right to left"</A>", top to bottom, with only "<A href="/tutorial/functions#modifiers">"one precedence rule"</A>". As operators are to the left of their operands, "{lang}" code reads a little bit like a Lisp, but with fewer parentheses."</p> - </div> - <div> - <Hd id="system-apis">"System APIs"</Hd> - <p>{lang}" has functions for spawning threads, interacting with the file system, communicating over network sockets, and "<A href="/docs/system">"more"</A>"."</p> - </div> - <div> - <Hd id="rust-ingegration">"Rust Integration"</Hd> - <p>{lang}" can be embedded in Rust programs "<a href="https://docs.rs/uiua">"as a library"</a>"."</p> - </div> - <div> - <Hd id="ffi">"FFI"</Hd> - <p>{lang}" has experimental support for calling functions from shared libraries through "<Prim prim=Sys(SysOp::Ffi)/>"."</p> - </div> - </div> - <div> - <div> - <Hd id="friendly-glyphs">"Friendly Glyphs"</Hd> - <p>{lang}" uses special characters for built-in functions that remind you what they do!"</p> - <Editor example="⚂ # Random number"/> - <Editor example="⇡8 # Range up to"/> - <Editor example="⇌ 1_2_3_4 # Reverse"/> - <Editor example="⌕ \"ab\" \"abracabra\" # Find"/> - <Editor example="⊟ 1_2_3 4_5_6 # Couple"/> - <p>"Unlike other array languages, "{lang}" does not have monadic and dyadic versions of each glyph. Every glyph does only one thing, so you don't need to parse an entire expression to know which version it is."</p> - </div> - <div> - <Hd id="unicode-formatter">"Unicode Formatter"</Hd> - <p>{lang}" has the terseness and expressivity afforded by Unicode glyphs without the need for special keyboard or editor support. Instead, the language comes with a formatter that converts the names of built-in functions into glyphs."</p> - <Editor example="floor*10repeatrand5" help={&["", "Click to format ⇡⇡⇡ "]}/> - </div> - <div> - <Hd id="multimedia-output">"Multimedia Output"</Hd> - <p>{lang}" has built-in facilities for generating images and audio. Just make arrays of the pixel data or audio samples. You can even make GIFs!"</p> - { - if cfg!(debug_assertions) { - None - } else { - Some(view!{ - <Editor example="⍉⊞<⊞+⇡3∿∩(÷25)⇡240⇡80"/> - <Editor example="÷3/+∿⊞×⊟×1.5.220×τ÷⟜⇡&asr"/> - <Editor example="Xy ← ⍉⍉⊞⊟.÷⟜⇡\nF ← ⍉◿1⊂⊃(+/÷|÷3+1∿×τ+)Xy\n≡F100 ÷⟜⇡10"/> - }) - } - } - <p>"The "{lang}" logo was made with "{lang}"! Check example 5 at the top of the page."</p> - </div> - <div> - <Hd id="language-server">"Language Server"</Hd> - <p>"The "{lang}" interpreter has a built-in language server that uses the "<a href="https://microsoft.github.io/language-server-protocol/">"Language Server Protocol"</a>", so you can "<A href="/install#editor-support">"use it with your favorite editor"</A>"."</p> - </div> - </div> - </div> - <div> - <Hd id="getting-started">"Getting Started"</Hd> - <p>"For more examples of what "{lang}" code looks like and what it can do, see the examples in the editor at the top of this page."</p> - <p>"For a quick overview of how the language works, see the "<A href="/tour">"Language Tour"</A>"."</p> - <p>"For a full tutorial, see the "<A href="/docs#tutorial">"Tutorial"</A>"."</p> - <p>"For a reference of all the built-in functions, the documentation has a "<A href="/docs#functions">"full list"</A>"."</p> - </div> - } -} - -#[component] -fn NotFound() -> impl IntoView { - view! { - <h1>"Page not found"</h1> - <Editor example="$ Where could it be?\n×101⧻⊜⧻≠@ ."/> - <h3><A href="/">"Go home"</A></h3> - } -} - -#[cfg(test)] -fn prim_html(prim: Primitive, glyph_only: bool, hide_docs: bool) -> String { - use uiua::PrimDoc; - - let symbol_class = format!("prim-glyph {}", uiua_editor::prim_class(prim)); - let symbol = prim.to_string(); - let name = if !glyph_only && symbol != prim.name() { - format!(" {}", prim.name()) - } else { - "".to_string() - }; - let href = format!("/docs/{}", prim.name()); - let mut title = String::new(); - if let Some(ascii) = prim.ascii() { - title.push_str(&format!("({ascii})")); - } - if prim.glyph().is_some() && glyph_only { - if !title.is_empty() { - title.push(' '); - } - title.push_str(prim.name()); - } - if let Primitive::Sys(op) = prim { - title.push_str(op.long_name()); - title.push(':'); - title.push('\n'); - } - if !hide_docs { - let doc = PrimDoc::from(prim); - if glyph_only && !title.is_empty() && !matches!(prim, Primitive::Sys(_)) { - title.push_str(": "); - } - title.push_str(&doc.short_text()); - } - if title.is_empty() { - format!( - r#"<a href="{href}"class="prim-code-a"> - <code><span class="{symbol_class}">{symbol}</span>{name}</code> - </a>"#, - ) - } else { - format!( - r#"<a href="{href}" class="prim-code-a"> - <code class="prim-code" data-title="{title}"><span class="{symbol_class}">{symbol}</span>{name}</code> - </a>"#, - ) - } -} - -#[component] -pub fn Prims<const N: usize>( - prims: [Primitive; N], - #[prop(optional)] show_names: bool, -) -> impl IntoView { - prims - .into_iter() - .map(|prim| view!(<Prim prim=prim glyph_only={!show_names}/>)) - .collect::<Vec<_>>() -} - -#[component] -#[allow(clippy::needless_lifetimes)] -fn Const<'a>(con: &'a ConstantDef) -> impl IntoView { - let name_class = binding_name_class(con.name); - let outer_class = if name_class.is_some() { - "prim-code-a code-background" - } else { - "prim-code-a" - }; - let inner_class = format!( - "prim-code stack-function {}", - name_class.unwrap_or_default() - ); - view! { - <a href={format!("/docs/constants#{}", con.name)} class=outer_class> - <code - id={con.name} - class=inner_class - data-title={ con.doc().lines().next().unwrap_or_default().to_string() } - >{ con.name }</code> - </a> - } -} - -#[track_caller] -#[allow(clippy::manual_map)] -fn get_element<T: JsCast>(id: &str) -> Option<T> { - if let Some(elem) = document().get_element_by_id(id) { - Some(elem.dyn_into().unwrap()) - } else { - None - } -} - -#[track_caller] -fn element<T: JsCast>(id: &str) -> T { - if let Some(elem) = get_element(id) { - elem - } else { - panic!("#{id} not found") - } -} - -#[component] -pub fn Tour() -> impl IntoView { - title_markdown("Language Tour", "/text/tour.md", View::default()) -} - -#[component] -pub fn PadPage() -> impl IntoView { - let src = pad_src(); - let version = format!("{} {}", lang(), uiua::VERSION); - let help = &[ - match lang() { - "Weewuh" => "Note: Weewuh is not yet stable", - _ => "Note: Uiua is not yet stable", - }, - &version, - ]; - view! { - <Title text=format!("Pad - {}", lang())/> - <Editor mode=EditorMode::Pad example={ &src } help=help/> - <br/> - <br/> - { - let date = Date::new_0(); - if date.get_month() == 11 || date.get_month() == 10 && date.get_date() > 15 { - let year = date.get_full_year(); - const CODE: &str = "6680-a10280cf"; - let copy_code = move |_| { - _ = window().navigator().clipboard().write_text(CODE) - }; - Some(view! { - <p>"Join the official "<a href={format!("https://adventofcode.com/{year}")}>"Advent of Code "{year}</a>" "{lang}" community leaderboard "<a href={format!("https://adventofcode.com/{year}/leaderboard/private")}>"here!"</a>"  "<button on:click=copy_code>"Copy code"</button></p> - }) - } else { - None - } - } - <br/> - <p>"You can load files into the pad by dragging and dropping them into the window."</p> - <p>"Replace "<code>"pad"</code>" in links with "<code>"embed"</code>" or "<code>"embedpad"</code>" to embed the editor."</p> - <p>"Keyboard shortcuts:"</p> - <code class="code-block"> - { EDITOR_SHORTCUTS } - </code> - <p>"Want a pad-like experience in the native interpreter? Try the "<code>"uiua -w"</code>" command to show output in a window."</p> - <p>"You can download the newest version of the native interpreter "<a href="https://github.com/uiua-lang/uiua/releases">"here"</a>"."</p> - } -} - -#[component] -pub fn EmbedPad() -> impl IntoView { - let src = pad_src(); - view! { - <Editor mode=EditorMode::Pad example={ &src }/> - } -} - -#[component] -pub fn Embed() -> impl IntoView { - let src = pad_src(); - view! { - <Editor mode=EditorMode::Example example={ &src }/> - } -} - -fn pad_src() -> String { - let mut src = use_query_map() - .with_untracked(|params| params.get("src").cloned()) - .unwrap_or_default(); - if let Some((_, encoded)) = src.split_once("__") { - // logging::log!("{:?}", encoded); - if let Ok(decoded) = URL_SAFE.decode(encoded.as_bytes()) { - src = String::from_utf8_lossy(&decoded).to_string(); - } - } else if let Ok(decoded) = URL_SAFE.decode(src.as_bytes()) { - src = String::from_utf8_lossy(&decoded).to_string(); - } - src -} - -#[test] -fn site() { - type Test = ( - std::path::PathBuf, - String, - std::thread::JoinHandle<(uiua::UiuaResult<uiua::Compiler>, bool)>, - ); - fn recurse_dir(path: &std::path::Path, threads: &mut Vec<Test>) -> std::io::Result<()> { - for entry in std::fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - if entry.file_type()?.is_file() { - for line in std::fs::read_to_string(&path)?.lines() { - let (code, should_fail) = - if let Some(code) = line.trim().strip_prefix(r#"<Editor example=""#) { - if let Some(code) = code.strip_suffix(r#""/>"#) { - (code, false) - } else if let Some(code) = code.strip_suffix(r#""/> // Should fail"#) { - (code, true) - } else { - continue; - } - } else if let Some(line) = - line.strip_prefix(" { ").and_then(|line| { - (line.split(", ").nth(2)) - .and_then(|rest| rest.strip_prefix('"')) - .and_then(|rest| rest.strip_suffix("\") }")) - }) - { - (line, false) - } else { - continue; - }; - let code = code - .replace("\\\\n", "<escaped-newline>") - .replace("\\n", "\n") - .replace("\\\"", "\"") - .replace("\\\\", "\\") - .replace("<escaped-newline>", "\\n"); - if code.contains("\"git:") - || [uiua::SysOp::AudioPlay, uiua::SysOp::GifShow] - .iter() - .any(|p| code.contains(p.name())) - { - continue; - } - threads.push(( - path.to_path_buf(), - code.clone(), - std::thread::spawn(move || { - ( - uiua::Uiua::with_backend( - uiua_editor::backend::WebBackend::default(), - ) - .run_str(&code), - should_fail, - ) - }), - )); - } - } else if entry.file_type()?.is_dir() { - recurse_dir(&path, threads)?; - } - } - Ok(()) - } - let mut threads = Vec::new(); - recurse_dir("src".as_ref(), &mut threads).unwrap(); - assert!(threads.len() > 50); - for (path, code, thread) in threads { - match thread.join().unwrap() { - (Err(e), false) => { - panic!( - "Test failed in {}\n{}\n{}", - path.display(), - code, - e.report() - ); - } - (Err(_), true) => {} - (Ok(mut comp), should_fail) => { - if let Some(diag) = comp.take_diagnostics().into_iter().next() { - if !should_fail { - panic!( - "Test failed in {}\n{}\n{}", - path.display(), - code, - diag.report() - ); - } - continue; - } - if should_fail { - panic!("Test should have failed in {}\n{}", path.display(), code); - } - } - } - } -} - -#[component] -#[allow(clippy::needless_lifetimes)] -fn Hd<'a>(id: &'a str, #[prop(optional)] class: &'a str, children: Children) -> impl IntoView { - let id = id.to_string(); - view! { - <h2 id={id.clone()}> - <a class={format!("header {class}")} href={ format!("#{id}") }>{children()}</a> - </h2> - } -} - -#[component] -#[allow(clippy::needless_lifetimes)] -fn Hd3<'a>(id: &'a str, #[prop(optional)] class: &'a str, children: Children) -> impl IntoView { - let id = id.to_string(); - view! { - <h3 id={id.clone()}> - <a class={format!("header {class}")} href={ format!("#{id}") }>{children()}</a> - </h3> - } -} - -#[cfg(test)] -#[test] -fn gen_primitives_json() { - use serde::*; - use std::{collections::BTreeMap, fs, ops::Not}; - use uiua::PrimDoc; - - #[derive(Serialize)] - struct PrimDef { - #[serde(skip_serializing_if = "Option::is_none")] - ascii: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - glyph: Option<char>, - #[serde(skip_serializing_if = "Option::is_none")] - args: Option<usize>, - #[serde(skip_serializing_if = "Option::is_none")] - outputs: Option<usize>, - #[serde(skip_serializing_if = "Option::is_none")] - modifier_args: Option<usize>, - class: String, - description: String, - #[serde(skip_serializing_if = "Not::not")] - experimental: bool, - #[serde(skip_serializing_if = "Not::not")] - deprecated: bool, - } - - let mut prims = BTreeMap::new(); - for prim in Primitive::all() { - let doc = PrimDoc::from(prim); - prims.insert( - prim.name(), - PrimDef { - ascii: prim.ascii().map(|a| a.to_string()), - glyph: prim.glyph(), - args: prim.args(), - outputs: prim.outputs(), - modifier_args: prim.modifier_args(), - class: format!("{:?}", prim.class()), - description: doc.short_text().into(), - experimental: prim.is_experimental(), - deprecated: prim.is_deprecated(), - }, - ); - } - let json = serde_json::to_string_pretty(&prims).unwrap(); - fs::write("primitives.json", json).unwrap(); -} - -#[component] -fn ScrollToHash() -> impl IntoView { - move || { - let location = use_location(); - create_effect(move |_| { - let hash = location.hash.get(); - if !hash.is_empty() { - set_timeout( - move || { - let id = hash.trim_start_matches('#'); - if let Some(elem) = get_element::<Element>(id) { - elem.scroll_into_view(); - } - }, - Duration::from_millis(0), - ) - } - }); - } -} - -#[component] -pub fn Challenge<'a, P: IntoView + 'static>( - number: u8, - prompt: P, - example: &'a str, - answer: &'a str, - tests: &'a [&'a str], - hidden: &'a str, - #[prop(optional)] default: &'a str, - #[prop(optional)] flip: bool, - #[prop(optional)] best_answer: &'a str, -) -> impl IntoView { - let def = ChallengeDef { - example: example.into(), - intended_answer: answer.into(), - best_answer: (!best_answer.is_empty()).then(|| best_answer.into()), - tests: tests.iter().copied().map(Into::into).collect(), - hidden: hidden.into(), - flip, - did_init_run: Cell::new(false), - }; - view! { - <div class="challenge"> - <h3>"Challenge "{number}</h3> - <p>"Write a program that "<strong>{prompt}</strong>"."</p> - <Editor challenge=def example=default/> - </div> - } -} diff --git a/site/src/markdown.rs b/site/src/markdown.rs index 5174ec18c..88b96f357 100644 --- a/site/src/markdown.rs +++ b/site/src/markdown.rs @@ -52,7 +52,7 @@ pub fn markdown_view(text: &str) -> View { node_view(root) } -#[cfg(test)] +#[cfg(feature = "gen_blog_html")] pub fn markdown_html(text: &str) -> String { let arena = Arena::new(); let text = text @@ -215,7 +215,7 @@ fn node_view<'a>(node: &'a AstNode<'a>) -> View { } } -#[cfg(test)] +#[cfg(feature = "gen_blog_html")] fn node_html<'a>(node: &'a AstNode<'a>) -> String { use uiua::{Compiler, PrimDoc, SafeSys, Uiua, UiuaErrorKind, Value}; use uiua_editor::prim_class; diff --git a/site/text/format_config.md b/site/text/format_config.md deleted file mode 100644 index 32da54a19..000000000 --- a/site/text/format_config.md +++ /dev/null @@ -1,62 +0,0 @@ - -# Uiua Formatter Configuration - -You can configure Uiua's formatter by creating a file called `.fmt.ua` in the directory from which you run the interpreter. This configuration file is also a Uiua program. - -Configuration options are specified by binding values to specific names. - -Example with default values: -```uiua -TrailingNewline ← 1 -CommentSpaceAfterHash ← 1 -MultilineIndent ← 2 -AlignComments ← 1 -IndentItemImports ← 1 -``` -The following configuration options are available: - -### TrailingNewline -Type: boolean - -Default: `1` - -Whether to add a trailing newline to the output. - ---- - -### CommentSpaceAfterHash -Type: boolean - -Default: `1` - -Whether to add a space after the `#` in comments. - ---- - -### MultilineIndent -Type: natural number - -Default: `2` - -The number of spaces to indent multiline arrays and functions - ---- - -### AlignComments -Type: boolean - -Default: `1` - -Whether to align consecutive end-of-line comments - ---- - -### IndentItemImports -Type: boolean - -Default: `1` - -Whether to indent item imports - ---- - diff --git a/src/format.rs b/src/format.rs index 76fa164a7..5ad222493 100644 --- a/src/format.rs +++ b/src/format.rs @@ -69,7 +69,6 @@ macro_rules! requirement { }; } -#[cfg(test)] macro_rules! param_type { (bool) => { "boolean" @@ -82,7 +81,6 @@ macro_rules! param_type { }; } -#[cfg(test)] macro_rules! default_to_uiua { ($default:expr) => {{ let default = format!("{:?}", $default); @@ -111,8 +109,9 @@ macro_rules! create_config { )* } - #[test] - fn generate_format_cfg_docs() { + #[doc(hidden)] + #[cfg(feature = "internal_fmt_doc")] + pub fn format_cfg_docs() -> String { paste! { let mut s: String = r#" # Uiua Formatter Configuration @@ -140,7 +139,7 @@ The following configuration options are available: s.push_str("\n---\n\n"); )* - fs::write("site/text/format_config.md", s).unwrap(); + s } } diff --git a/src/run_prim.rs b/src/run_prim.rs index a59364786..87e11c7e8 100644 --- a/src/run_prim.rs +++ b/src/run_prim.rs @@ -2326,265 +2326,4 @@ mod tests { )); assert_eq!(split_name("foo"), None); } - - #[cfg(test)] - #[test] - fn gen_grammar_file() { - use crate::PrimClass; - fn gen_group(prims: impl Iterator<Item = Primitive> + Clone, additional: &str) -> String { - let glyphs = prims - .clone() - .flat_map(|p| { - p.glyph() - .into_iter() - .chain(p.ascii().into_iter().flat_map(|ascii| { - Some(ascii.to_string()) - .filter(|s| s.len() == 1) - .into_iter() - .flat_map(|s| s.chars().collect::<Vec<_>>()) - })) - }) - .collect::<String>() - .replace('\\', "\\\\\\\\") - .replace('-', "\\\\-") - .replace('*', "\\\\*") - .replace('^', "\\\\^"); - let format_names: Vec<_> = prims - .clone() - .map(|p| { - let name = p.name(); - let min_len = if name.starts_with('&') { - name.len() - } else { - (2..=name.len()) - .find(|&n| Primitive::from_format_name(&name[..n]) == Some(p)) - .unwrap() - }; - let mut start: String = name.chars().take(min_len).collect(); - let mut end = String::new(); - for c in name.chars().skip(min_len) { - start.push('('); - start.push(c); - end.push_str(")?"); - } - format!("{start}{end}") - }) - .collect(); - let format_names = format_names.join("|"); - let mut literal_names: Vec<_> = prims - .map(|p| p.names()) - .filter(|p| p.ascii.is_none() && p.glyph.is_none()) - .map(|n| format!("|{}", n.text)) - .collect(); - literal_names.sort_by_key(|s| s.len()); - literal_names.reverse(); - let literal_names = literal_names.join(""); - format!( - r#"[{glyphs}]|(?<![a-zA-Z$])({format_names}{literal_names})(?![a-zA-Z]){additional}"# - ) - } - - let stack_functions = gen_group( - Primitive::non_deprecated() - .filter(|p| { - [PrimClass::Stack, PrimClass::Debug].contains(&p.class()) - && p.modifier_args().is_none() - }) - .chain(Some(Primitive::Identity)), - "", - ); - let noadic_functions = gen_group( - Primitive::non_deprecated().filter(|p| { - ![PrimClass::Stack, PrimClass::Debug, PrimClass::Constant].contains(&p.class()) - && p.modifier_args().is_none() - && p.args() == Some(0) - }), - "", - ); - let monadic_functions = gen_group( - Primitive::non_deprecated().filter(|p| { - ![PrimClass::Stack, PrimClass::Debug, PrimClass::Planet].contains(&p.class()) - && p.modifier_args().is_none() - && p.args() == Some(1) - }), - "|⋊[a-zA-Z]*", - ); - let dyadic_functions = gen_group( - Primitive::non_deprecated().filter(|p| { - ![PrimClass::Stack, PrimClass::Debug].contains(&p.class()) - && p.modifier_args().is_none() - && p.args() == Some(2) - }), - "", - ); - let monadic_modifiers = gen_group( - Primitive::non_deprecated().filter(|p| matches!(p.modifier_args(), Some(1))), - "", - ); - let dyadic_modifiers: String = gen_group( - Primitive::non_deprecated().filter(|p| matches!(p.modifier_args(), Some(n) if n >= 2)), - "", - ); - - let text = format!( - r##"{{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "Uiua", - "firstLineMatch": "^#!/.*\\buiua\\b", - "fileTypes": [ - "ua" - ], - "patterns": [ - {{ - "include": "#comments" - }}, - {{ - "include": "#strings-multiline-format" - }}, - {{ - "include": "#strings-multiline" - }}, - {{ - "include": "#strings-format" - }}, - {{ - "include": "#strings-normal" - }}, - {{ - "include": "#characters" - }}, - {{ - "include": "#labels" - }}, - {{ - "include": "#module_delim" - }}, - {{ - "include": "#strand" - }}, - {{ - "include": "#stack" - }}, - {{ - "include": "#noadic" - }}, - {{ - "include": "#monadic" - }}, - {{ - "include": "#dyadic" - }}, - {{ - "include": "#mod1" - }}, - {{ - "include": "#mod2" - }}, - {{ - "include": "#idents" - }}, - {{ - "include": "#numbers" - }} - ], - "repository": {{ - "idents": {{ - "name": "variable.parameter.uiua", - "match": "\\b[a-zA-Z]+['′″‴]*(₋?[₀₁₂₃₄₅₆₇₈₉]|,`?\\d+)*[!‼]*\\b" - }}, - "comments": {{ - "name": "comment.line.uiua", - "match": "(#.*$|$[a-zA-Z]*)" - }}, - "strings-normal": {{ - "name": "constant.character.escape", - "begin": "\"", - "end": "\"", - "patterns": [ - {{ - "name": "string.quoted", - "match": "\\\\[\\\\\"0nrt]" - }} - ] - }}, - "strings-format": {{ - "name": "constant.character.escape", - "begin": "\\$\"", - "end": "\"", - "patterns": [ - {{ - "name": "string.quoted", - "match": "\\\\[\\\\\"0nrt_]" - }}, - {{ - "name": "constant.numeric", - "match": "(?<!\\\\)_" - }} - ] - }}, - "strings-multiline": {{ - "name": "constant.character.escape", - "begin": "\\$ ", - "end": "$" - }}, - "strings-multiline-format": {{ - "name": "constant.character.escape", - "begin": "\\$\\$ ", - "end": "$", - "patterns": [ - {{ - "name": "constant.numeric", - "match": "(?<!\\\\)_" - }} - ] - }}, - "characters": {{ - "name": "constant.character.escape", - "match": "@(\\\\(x[0-9A-Fa-f]{{2}}|u[0-9A-Fa-f]{{4}}|.)|.)" - }}, - "labels": {{ - "name": "label.uiua", - "match": "\\$[a-zA-Z]*" - }}, - "numbers": {{ - "name": "constant.numeric.uiua", - "match": "[`¯]?(\\d+|η|π|τ|∞|eta|pi|tau|inf(i(n(i(t(y)?)?)?)?)?)([./]\\d+|e[+-]?\\d+)?" - }}, - "strand": {{ - "name": "comment.line", - "match": "(_|‿)" - }}, - "module_delim": {{ - "match": "---" - }}, - "stack": {{ - "match": "{stack_functions}" - }}, - "noadic": {{ - "name": "entity.name.tag.uiua", - "match": "{noadic_functions}" - }}, - "monadic": {{ - "name": "string.quoted", - "match": "{monadic_functions}" - }}, - "dyadic": {{ - "name": "entity.name.function.uiua", - "match": "{dyadic_functions}" - }}, - "mod1": {{ - "name": "entity.name.type.uiua", - "match": "{monadic_modifiers}" - }}, - "mod2": {{ - "name": "keyword.control.uiua", - "match": "{dyadic_modifiers}" - }} - }}, - "scopeName": "source.uiua" -}}"## - ); - - std::fs::write("uiua.tmLanguage.json", text).expect("Failed to write grammar file"); - } }