diff --git a/codemod/.envrc b/codemod/.envrc new file mode 100644 index 0000000..8acb3a3 --- /dev/null +++ b/codemod/.envrc @@ -0,0 +1,10 @@ +source_up + +files=(../../flake.nix flake-module.nix Cargo.lock) +if type nix_direnv_watch_file &>/dev/null; then + nix_direnv_watch_file "${files[@]}" +else + watch_file "${files[@]}" +fi + +use flake .#codemod --builders '' diff --git a/codemod/.gitignore b/codemod/.gitignore new file mode 100644 index 0000000..8ea0ee8 --- /dev/null +++ b/codemod/.gitignore @@ -0,0 +1,2 @@ +/target +result* diff --git a/codemod/Cargo.lock b/codemod/Cargo.lock new file mode 100644 index 0000000..c96866c --- /dev/null +++ b/codemod/Cargo.lock @@ -0,0 +1,221 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "codemod" +version = "0.1.0" +dependencies = [ + "expect-test", + "regex", + "rnix", + "rowan", + "textwrap", + "walkdir", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "dissimilar" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" + +[[package]] +name = "expect-test" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rnix" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f" +dependencies = [ + "rowan", +] + +[[package]] +name = "rowan" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf" +dependencies = [ + "countme", + "hashbrown", + "memoffset", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/codemod/Cargo.toml b/codemod/Cargo.toml new file mode 100644 index 0000000..b73969e --- /dev/null +++ b/codemod/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "codemod" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "1.9.5" +rnix = "0.11.0" +rowan = { version = "*" } +textwrap = "0.16.0" +walkdir = "2.4.0" + +[dev-dependencies] +expect-test = "1.4.0" diff --git a/codemod/README.md b/codemod/README.md new file mode 100644 index 0000000..a7b1801 --- /dev/null +++ b/codemod/README.md @@ -0,0 +1,64 @@ +# Doc-comments Codemod + +A simple codemod based on [rnix](https://github.com/nix-community/rnix-parser). +It migrates all comments automatically into the new markdown format. + +## Features + +- Fully automatic. +- Changes all files from all directories (if needed). +- Markdown output. +- Re-aligns the indentation. + +## Example + +`input` +```nix + /* Throw if pred is false, else return pred. + Intended to be used to augment asserts with helpful error messages. + + Example: + assertMsg false "nope" + stderr> error: nope + + assert assertMsg ("foo" == "bar") "foo is not bar, silly"; "" + stderr> error: foo is not bar, silly + + Type: + assertMsg :: Bool -> String -> Bool + */ + assertMsg = +``` + +-> + +`output` +````nix + /** + Throw if pred is false, else return pred. + Intended to be used to augment asserts with helpful error messages. + + # Example + + ```nix + assertMsg false "nope" + stderr> error: nope + + assert assertMsg ("foo" == "bar") "foo is not bar, silly"; "" + stderr> error: foo is not bar, silly + ``` + + # Type + + ``` + assertMsg :: Bool -> String -> Bool + ``` + */ + assertMsg = +```` + +## Development + +Enter the devshell + +`nix develop .#codemod` diff --git a/codemod/flake-module.nix b/codemod/flake-module.nix new file mode 100644 index 0000000..17bd042 --- /dev/null +++ b/codemod/flake-module.nix @@ -0,0 +1,45 @@ +{ inputs, ... }: { + perSystem = { self', inputs', pkgs, system, ... }: + let + craneLib = inputs.crane.lib.${system}; + src = craneLib.cleanCargoSource (craneLib.path ./.); + + commonArgs = { + inherit src; + strictDeps = true; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + }; + + codemod = craneLib.buildPackage commonArgs; + + + nixpkgs-migrated = pkgs.stdenv.mkDerivation { + name = "nixpkgs-migrated"; + src = inputs.nixpkgs; + buildPhase = '' + ${self'.packages.codemod}/bin/codemod . + cp -r . $out + ''; + }; + + checks = { + inherit codemod; + codemod-clippy = craneLib.cargoClippy (commonArgs // { + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }); + codemod-fmt = craneLib.cargoFmt { inherit src; }; + codemod-nextest = craneLib.cargoNextest (commonArgs // { + partitions = 1; + partitionType = "count"; + }); + }; + in + { + packages = { inherit codemod nixpkgs-migrated; }; + inherit checks; + devShells.codemod = craneLib.devShell { + # Inherit inputs from checks. + inherit checks; + }; + }; +} diff --git a/codemod/src/main.rs b/codemod/src/main.rs new file mode 100644 index 0000000..c993a22 --- /dev/null +++ b/codemod/src/main.rs @@ -0,0 +1,473 @@ +use regex::Regex; +use rnix::ast::{AstToken, AttrpathValue, Comment, Expr, Lambda, Param}; +use rnix::{SyntaxKind, SyntaxNode, SyntaxToken}; +use rowan::{ast::AstNode, GreenToken, NodeOrToken, WalkEvent}; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::println; +use std::{env, fs}; +use textwrap::{dedent, indent}; +use walkdir::WalkDir; + +const EXAMPLE_LANG: &str = "nix"; +const TYPE_LANG: &str = ""; + +/// Represent a single function argument name and its (optional) +/// doc-string. +#[derive(Clone, Debug)] +pub struct SingleArg { + pub name: String, + pub doc: Option, +} + +/// Represent a function argument, which is either a flat identifier +/// or a pattern set. +#[derive(Clone, Debug)] +pub enum Argument { + /// Flat function argument (e.g. `n: n * 2`). + Flat(SingleArg), + + /// Pattern function argument (e.g. `{ name, age }: ...`) + Pattern(Vec), +} + +/// +fn handle_indentation(raw: &str) -> Option { + let result: String = match raw.split_once('\n') { + Some((first, rest)) => { + format!("{}\n{}", first.trim_start(), dedent(rest)) + } + None => raw.into(), + }; + + Some(result.trim().to_owned()).filter(|s| !s.is_empty()) +} + +/// Retrieve documentation comments. +fn retrieve_doc_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option { + // if the current node has a doc comment it'll be immediately preceded by that comment, + // or there will be a whitespace token and *then* the comment tokens before it. We merge + // multiple line comments into one large comment if they are on adjacent lines for + // documentation simplicity. + let mut token = node.first_token()?.prev_token()?; + if token.kind() == SyntaxKind::TOKEN_WHITESPACE { + token = token.prev_token()?; + } + if token.kind() != SyntaxKind::TOKEN_COMMENT { + return None; + } + + // if we want to ignore line comments (eg because they may contain deprecation + // comments on attributes) we'll backtrack to the first preceding multiline comment. + while !allow_line_comments && token.text().starts_with('#') { + token = token.prev_token()?; + if token.kind() == SyntaxKind::TOKEN_WHITESPACE { + token = token.prev_token()?; + } + if token.kind() != SyntaxKind::TOKEN_COMMENT { + return None; + } + } + + if token.text().starts_with("/*") { + return Some(Comment::cast(token)?.text().to_string()); + } + + // backtrack to the start of the doc comment, allowing only adjacent line comments. + // we don't care much about optimization here, doc comments aren't long enough for that. + if token.text().starts_with('#') { + let mut result: String = String::new(); + while let Some(comment) = Comment::cast(token) { + if !comment.syntax().text().starts_with('#') { + break; + } + result.insert_str(0, comment.text().trim()); + let ws = match comment.syntax().prev_token() { + Some(t) if t.kind() == SyntaxKind::TOKEN_WHITESPACE => t, + _ => break, + }; + // only adjacent lines continue a doc comment, empty lines do not. + match ws.text().strip_prefix('\n') { + Some(trail) if !trail.contains('\n') => result.insert(0, ' '), + _ => break, + } + token = match ws.prev_token() { + Some(c) => c, + _ => break, + }; + } + return Some(result); + } + + None +} + +/// Copied from nixdoc. +/// Traverse a Nix lambda and collect the identifiers of arguments +/// until an unexpected AST node is encountered. +fn collect_lambda_args(mut lambda: Lambda) -> Vec { + let mut args = vec![]; + + loop { + match lambda.param().unwrap() { + // a variable, e.g. `id = x: x` + Param::IdentParam(id) => { + args.push(Argument::Flat(SingleArg { + name: id.to_string(), + // doc: + doc: handle_indentation( + &retrieve_doc_comment(id.syntax(), true).unwrap_or_default(), + ), + })); + } + // an attribute set, e.g. `foo = { a }: a` + Param::Pattern(pat) => { + // collect doc-comments for each attribute in the set + let pattern_vec: Vec<_> = pat + .pat_entries() + .map(|entry| SingleArg { + name: entry.ident().unwrap().to_string(), + doc: None, // handle_indentation( + // &retrieve_doc_comment(entry.syntax(), true).unwrap_or_default(), + // ), + }) + .collect(); + + args.push(Argument::Pattern(pattern_vec)); + } + } + + // Curried or not? + match lambda.body() { + Some(Expr::Lambda(inner)) => lambda = inner, + _ => break, + } + } + + args +} + +fn parse_doc_comment(raw: &str, indent: usize) -> String { + enum ParseState { + Doc, + Type, + Example, + } + let left = " ".repeat(indent); + + let mut doc = String::new(); + let mut doc_type = String::new(); + let mut example = String::new(); + let mut state = ParseState::Doc; + + for line in raw.lines() { + let mut line = line.trim_end(); + + let trimmed = line.clone().trim(); + + if trimmed.starts_with("Type:") { + state = ParseState::Type; + line = &trimmed[5..]; // trim 'Type:' + } + + if trimmed.starts_with("Example:") { + state = ParseState::Example; + line = &trimmed[8..]; // trim 'Example:' + } + if trimmed.starts_with("Examples:") { + state = ParseState::Example; + line = &trimmed[9..]; // trim 'Examples:' + } + + let trimmed = line.trim(); + + let formatted = if !trimmed.is_empty() { + format!("{left}{trimmed}\n") + } else { + format!("") + }; + match state { + // important: trim only trailing whitespaces; as leading ones might be markdown formatting or code examples. + ParseState::Type => { + doc_type.push_str(&format!("{line}\n")); + } + ParseState::Doc => { + doc.push_str(&formatted); + } + ParseState::Example => { + example.push_str(&format!("{line}\n")); + } + } + } + let f = |s: String| { + if s.is_empty() { + None + } else { + return Some(s.trim().to_owned()); + } + }; + let mut markdown = format!("{left}{}", f(doc).unwrap_or("".to_owned())); + // example and type can contain indented code + let formatted_example = format_code(example, indent); + let formatted_type = format_code(doc_type, indent); + + if let Some(example) = f(formatted_example) { + markdown.push_str(&format!("\n\n{left}# Example")); + markdown.push_str(&format!( + "\n\n{left}```{EXAMPLE_LANG}\n{left}{example}\n{left}```" + )); + } + + if let Some(doc_type) = f(formatted_type) { + markdown.push_str(&format!("\n\n{left}# Type")); + markdown.push_str(&format!( + "\n\n{left}```{TYPE_LANG}\n{left}{doc_type}\n{left}```" + )); + } + + markdown +} + +fn get_argument_docs(token: &SyntaxToken, ident: &str) -> Option { + let mut step = token.next_sibling_or_token(); + + // Find the Expr that is a lambda. + let doc_expr = loop { + if step.is_none() { + // If there is no next token or node + break None; + } else if let Some(NodeOrToken::Node(ref node)) = step { + match node.kind() { + // SyntaxKind::NODE_LAMBDA => break Some(node.clone()), + SyntaxKind::NODE_ATTRPATH_VALUE => { + if let Some(value) = AttrpathValue::cast(node.clone()) { + break value.value(); + } else { + break None; + } + } + _ => {} + }; + } else { + } + step = step.unwrap().next_sibling_or_token(); + }; + + let mut argument_docs: Option = None; + if let Some(Expr::Lambda(l)) = doc_expr { + let args = collect_lambda_args(l); + let mut docs = String::new(); + for arg in args { + match arg { + Argument::Flat(single_arg) => { + docs.push_str(&format!( + "{ident}- [{}] {}\n", + single_arg.name, + single_arg + .doc + .map(|ref body| indent_list_item_content(body, ident)) + .unwrap_or(String::from("")), + )); + } + Argument::Pattern(_pattern) => (), + } + } + argument_docs = Some(docs); + } + return argument_docs; +} + +/// Takes care of markdown list indentation +/// Dont indent the first line +/// indent the second line, by the parent level to continue the list. +fn indent_list_item_content(body: &str, indent: &str) -> String { + let mut res = String::from(""); + for (line_nr, line) in body.lines().enumerate() { + if line_nr > 0 { + res.push_str(&format!("{} {}\n", indent, line.trim())); + } else { + res.push_str(&format!("{}",line.trim())) + } + } + res +} + +fn format_comment(text: &str, token: &SyntaxToken) -> String { + let content = text.strip_prefix("/*").unwrap().strip_suffix("*/").unwrap(); + let mut whitespace = ""; + let prev = &token.prev_token(); + + if let Some(prev) = prev { + whitespace = prev.text(); + } + let stripped = Regex::new(r#" +"#).unwrap().replace_all(whitespace, ""); + let indentation = (whitespace.len() - stripped.len()) / 2 * 2; + + let indent_1 = " ".repeat(indentation); + let indent_2 = " ".repeat(indentation + 2); + + let lines: Vec = content + .lines() + .map(|content| format!("{}{}", indent_2, content)) + .collect(); + let mut markdown = parse_doc_comment(&lines.join("\n"), indentation + 2); + + if let Some(argument_docs) = get_argument_docs(token, &indent_2) { + markdown.push_str(&format!("\n\n{indent_2}# Arguments")); + markdown.push_str(&format!("\n\n{argument_docs}")); + } + + return format!("/**\n{}\n{}*/", markdown, indent_1); +} + +fn format_code(text: String, ident: usize) -> String { + let mut content = text + .trim_end_matches("\n") + .trim_start_matches("\n") + .to_owned(); + + while let Some(stripped) = strip_column(&content) { + content = stripped; + } + + let mut result = String::new(); + let left: String = " ".repeat(ident); + for line in content.lines() { + if line.is_empty() { + result.push_str(&format!("\n")); + } else { + result.push_str(&format!("{left}{line}\n")); + } + } + + result +} + +fn strip_column(text: &str) -> Option { + let mut result: Vec<&str> = vec![]; + + let mut any_non_whitespace = false; + + for line in text.lines() { + if line.is_empty() { + continue; + } + if let Some(_) = line.strip_prefix(" ") { + } else { + any_non_whitespace = true; + } + } + + if !any_non_whitespace && !text.is_empty() { + for line in text.lines() { + if let Some(stripped) = line.strip_prefix(" ") { + result.push(stripped); + } + } + return Some(result.join("\n")); + } + return None; +} + +fn replace_first_comment(syntax: &SyntaxNode) -> Option { + let mut result = None; + for ev in syntax.preorder_with_tokens() { + match ev { + WalkEvent::Enter(node_or_token) => match node_or_token { + NodeOrToken::Token(token) => match token.kind() { + SyntaxKind::TOKEN_COMMENT => { + if token.text().starts_with("/**") || token.text().starts_with("#") { + // Already a doc-comment or not supposed to be migrated + continue; + } + let replacement: GreenToken = GreenToken::new( + rowan::SyntaxKind(token.kind() as u16), + &format_comment(token.text(), &token), + ); + let green = token.replace_with(replacement); + let updated = syntax.replace_with(green); + + result = Some(rnix::SyntaxNode::new_root(updated)); + break; + } + _ => continue, + }, + _ => continue, + }, + _ => continue, + }; + } + result +} + +fn main() { + let args: Vec = env::args().collect(); + + if let Some(path) = &args.get(1) { + println!("trying to read path: {path}"); + for entry in WalkDir::new(path) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let f_name = entry.file_name().to_string_lossy(); + + if f_name.ends_with(".nix") { + modify_file(entry.path().to_path_buf()); + } + } + } else { + println!("Usage: codemod "); + } +} + +fn modify_file(file_path: PathBuf) { + let contents = fs::read_to_string(&file_path); + if let Err(_) = contents { + println!("Could not read the file {:?}", file_path); + return; + } + let root = rnix::Root::parse(&contents.unwrap()).ok(); + + if let Err(err) = &root { + println!( + "{}", + format!( + "failed to parse input of file: {:?} \n\ngot error: {}", + file_path, + err.to_string() + ) + ); + return; + } + + println!("Syntax \n {:?}", root.clone().unwrap().syntax().clone()); + let syntax = root.unwrap().syntax().clone_for_update(); + let mut maybe_replaced = replace_first_comment(&syntax); + let mut count = 0; + let r: Option = loop { + if let Some(replaced) = maybe_replaced { + // Maybe we can replace more + count += 1; + let result = replace_first_comment(&replaced); + + // If we cannot replace more comments + if result.is_none() { + break Some(replaced); + } + maybe_replaced = result; + } else { + break None; + } + }; + + let display_name = file_path.to_str().unwrap(); + if let Some(updates) = r { + let mut file = File::create(&file_path).unwrap(); + file.write_all(updates.text().to_string().as_bytes()).ok(); + println!("{display_name} - Changed {count} comments"); + } else { + println!("{display_name} - Doing nothing."); + } +} diff --git a/codemod/test/args.nix b/codemod/test/args.nix new file mode 100644 index 0000000..e7d792c --- /dev/null +++ b/codemod/test/args.nix @@ -0,0 +1,59 @@ +{ + /* + Reduce a list by applying a binary operator from left to right, + starting with an initial accumulator. + + Before each application of the operator, the accumulator value is evaluated. + This behavior makes this function stricter than [`foldl`](#function-library-lib.lists.foldl). + + Unlike [`builtins.foldl'`](https://nixos.org/manual/nix/unstable/language/builtins.html#builtins-foldl'), + the initial accumulator argument is evaluated before the first iteration. + + A call like + + ```nix + foldl' op acc₀ [ x₀ x₁ x₂ ... xₙ₋₁ xₙ ] + ``` + + is (denotationally) equivalent to the following, + but with the added benefit that `foldl'` itself will never overflow the stack. + + ```nix + let + acc₁ = builtins.seq acc₀ (op acc₀ x₀ ); + acc₂ = builtins.seq acc₁ (op acc₁ x₁ ); + acc₃ = builtins.seq acc₂ (op acc₂ x₂ ); + ... + accₙ = builtins.seq accₙ₋₁ (op accₙ₋₁ xₙ₋₁); + accₙ₊₁ = builtins.seq accₙ (op accₙ xₙ ); + in + accₙ₊₁ + + # Or ignoring builtins.seq + op (op (... (op (op (op acc₀ x₀) x₁) x₂) ...) xₙ₋₁) xₙ + ``` + + Type: foldl' :: (acc -> x -> acc) -> acc -> [x] -> acc + + Example: + foldl' (acc: x: acc + x) 0 [1 2 3] + => 6 + */ + foldl' = + /* The binary operation to run, where the two arguments are: + + 1. `acc`: The current accumulator value: Either the initial one for the first iteration, or the result of the previous iteration + 2. `x`: The corresponding list element for this iteration + */ + op: + # The initial accumulator value + acc: + # The list to fold + list: + + # The builtin `foldl'` is a bit lazier than one might expect. + # See https://github.com/NixOS/nix/pull/7158. + # In particular, the initial accumulator value is not forced before the first iteration starts. + builtins.seq acc + (builtins.foldl' op acc list); +} diff --git a/codemod/test/args/default.nix b/codemod/test/args/default.nix new file mode 100644 index 0000000..ccc082c --- /dev/null +++ b/codemod/test/args/default.nix @@ -0,0 +1,17 @@ +{ + /* + Header + + Example: + assertMsg false "nope" + + Type: + assertMsg :: Bool -> String -> Bool + + */ + stuff = + # a arg + a: + # b arg + b: a; +} diff --git a/codemod/test/nested/sample.nix b/codemod/test/nested/sample.nix new file mode 100644 index 0000000..b3122f6 --- /dev/null +++ b/codemod/test/nested/sample.nix @@ -0,0 +1,36 @@ +{ + /** + Header + + # Example + + ```nix + format me + nested 1 + nested 2 + ``` + + # Type + + ``` + some :: { + nested :: Number; + } + ``` + */ + stuff = 1; + /* Throw if pred is false, else return pred. + Intended to be used to augment asserts with helpful error messages. + + Example: + assertMsg false "nope" + stderr> error: nope + + assert assertMsg ("foo" == "bar") "foo is not bar, silly"; "" + stderr> error: foo is not bar, silly + + Type: + assertMsg :: Bool -> String -> Bool + */ + fun = true; +} diff --git a/codemod/test/simple.nix b/codemod/test/simple.nix new file mode 100644 index 0000000..b771cf0 --- /dev/null +++ b/codemod/test/simple.nix @@ -0,0 +1,22 @@ +/** + Map each attribute in the given set and merge them into a new attribute set. + + # Example + + ```nix + concatMapAttrs + (name: value: { + ${name} = value; + ${name + value} = value; + }) + { x = "a"; y = "b"; } + => { x = "a"; xa = "a"; y = "b"; yb = "b"; } + ``` + + # Type + + ``` + concatMapAttrs :: (String -> a -> AttrSet) -> AttrSet -> AttrSet + ``` + */ +1 diff --git a/flake.lock b/flake.lock index 5c4c704..af436db 100644 --- a/flake.lock +++ b/flake.lock @@ -200,22 +200,6 @@ "type": "indirect" } }, - "nixpkgs-migrated": { - "locked": { - "lastModified": 1699884649, - "narHash": "sha256-HF1iNm+SqZJtUgoi57Mk21jDsgeybIcopDwaNFLqexc=", - "owner": "hsjobeki", - "repo": "nixpkgs", - "rev": "047dce513a20231fde99b1e9b950ab6b562b27b0", - "type": "github" - }, - "original": { - "owner": "hsjobeki", - "ref": "migrate-doc-comments", - "repo": "nixpkgs", - "type": "github" - } - }, "nixpkgs-regression": { "locked": { "lastModified": 1643052045, @@ -295,7 +279,6 @@ "nix": "nix", "nixpkgs": "nixpkgs_2", "nixpkgs-master": "nixpkgs-master", - "nixpkgs-migrated": "nixpkgs-migrated", "pre-commit-hooks": "pre-commit-hooks", "treefmt-nix": "treefmt-nix" } diff --git a/flake.nix b/flake.nix index 89a6834..443cfe3 100644 --- a/flake.nix +++ b/flake.nix @@ -3,9 +3,8 @@ inputs = { nixpkgs.url = "nixpkgs/nixos-unstable"; nixpkgs-master.url = "nixpkgs/master"; - nixpkgs-migrated.url = "github:hsjobeki/nixpkgs/?ref=migrate-doc-comments"; - # A custom nix verison, to introspect lambda values. + # A custom nix version, to introspect lambda values. nix.url = "github:hsjobeki/nix/?ref=feat/positions"; pre-commit-hooks = { @@ -35,6 +34,7 @@ ./pasta/flake-module.nix ./pesto/flake-module.nix # Deprecated. Will be removed. + ./codemod/flake-module.nix ./indexer/flake-module.nix ./scripts/flake-module.nix ]; diff --git a/pasta/flake-module.nix b/pasta/flake-module.nix index c66520e..0a5ce6d 100644 --- a/pasta/flake-module.nix +++ b/pasta/flake-module.nix @@ -1,11 +1,18 @@ { inputs, ... }: { - perSystem = { self', inputs', pkgs, ... }: + perSystem = { self', inputs', pkgs, lib, ... }: let nix = inputs'.nix.packages.nix-clangStdenv; - nixpkgs = inputs.nixpkgs-migrated; + nixpkgs = self'.packages.nixpkgs-migrated; in { packages = { + pasta-meta = pkgs.stdenv.mkDerivation { + name = "pasta-meta"; + src = ./.; + buildPhase = '' + echo "\"${builtins.toJSON inputs.nixpkgs-master.rev}\"" > $out + ''; + }; pasta = pkgs.callPackage ./default.nix { inherit nixpkgs nix pkgs; }; }; devShells.pastaMaker = pkgs.callPackage ./shell.nix { inherit pkgs nix; }; diff --git a/pasta/src/eval.nix b/pasta/src/eval.nix index 83c3716..324ba39 100644 --- a/pasta/src/eval.nix +++ b/pasta/src/eval.nix @@ -28,11 +28,15 @@ let rustTools = collectFns pkgs.rustPackages { initialPath = [ "pkgs" "rustPackages" ]; }; - stdenvTools = getDocsFromSet pkgs.stdenv [ "pkgs" "stdenv" ]; - ############# Non-recursive analysis sets + ############# Non-recursive analysis sets (pkgs.) + # pkgs cannot be analysed recursively + # nested documentation items must be configured specifically + stdenvTools = getDocsFromSet pkgs.stdenv [ "pkgs" "stdenv" ]; pkgs = getDocsFromSet pkgs [ "pkgs" ]; dockerTools = getDocsFromSet pkgs.dockerTools [ "pkgs" "dockerTools" ]; + writers = getDocsFromSet pkgs.writers [ "pkgs" "writers" ]; + haskellLib = getDocsFromSet pkgs.haskell.lib [ "pkgs" "haskell" "lib" ]; pythonTools = getDocsFromSet pkgs.pythonPackages [ "pkgs" "pythonPackages" ]; builtins = diff --git a/pesto/flake-module.nix b/pesto/flake-module.nix index 43c75d9..8c49501 100644 --- a/pesto/flake-module.nix +++ b/pesto/flake-module.nix @@ -11,6 +11,16 @@ }; pesto = craneLib.buildPackage commonArgs; + + data-json = pkgs.stdenv.mkDerivation { + name = "nixpkgs-migrated"; + src = inputs.nixpkgs; + buildPhase = '' + ${pesto}/bin/pesto --pos-file ${self'.packages.pasta} --format json $out + ''; + }; + + checks = { inherit pesto; pesto-clippy = craneLib.cargoClippy (commonArgs // { @@ -24,7 +34,7 @@ }; in { - packages = { inherit pesto; }; + packages = { inherit pesto data-json; }; inherit checks; devShells.pesto = craneLib.devShell { # Inherit inputs from checks. diff --git a/pesto/src/alias.rs b/pesto/src/alias.rs index 3fc6bc2..6f9813f 100644 --- a/pesto/src/alias.rs +++ b/pesto/src/alias.rs @@ -31,30 +31,35 @@ pub fn find_aliases(item: &Docs, list: &Vec<&Docs>) -> AliasList { if item.path == other.path { return None; } - // We cannot safely compare using the current value introspection if countApplied not eq 0. - if (s_meta.countApplied != Some(0)) - // Use less accurate name (only) aliases. This can lead to potentially false positives. - // e.g. lib.lists.last <=> lib.last - // comparing only the "last" string. Don't use any introspection - // TODO: find out how to compare partially applied values. - // A correct solution would include comparing the upValues ? - && item.path.last() == other.path.last() - { - return Some(other.path.clone()); - } - return match s_meta.isPrimop { - true => { + + return match (o_meta.isPrimop, s_meta.isPrimop) { + (true, true) => { let is_empty = match &s_meta.content { Some(c) => c.is_empty(), None => true, }; + if s_meta.countApplied != Some(0) + && s_meta.countApplied == o_meta.countApplied + { + if item.path.last() == other.path.last() { + return Some(other.path.clone()); + } else { + return None; + } + } - if o_meta.isPrimop && o_meta.content == s_meta.content && !is_empty { + if o_meta.content == s_meta.content && !is_empty { return Some(other.path.clone()); } None } - false => { + (false, false) => { + if s_meta.countApplied != Some(0) { + if item.path.last() == other.path.last() { + return Some(other.path.clone()); + } + } + if s_meta.position == o_meta.position && s_meta.countApplied == Some(0) && s_meta.countApplied == o_meta.countApplied @@ -63,6 +68,7 @@ pub fn find_aliases(item: &Docs, list: &Vec<&Docs>) -> AliasList { } None } + _ => None, }; } None @@ -101,7 +107,8 @@ pub fn categorize(data: &Vec) -> FnCategories { for item in data.iter() { if let Some(lambda) = &item.docs.lambda { match lambda.countApplied { - Some(0) | None => { + // Some(0) | None => { + Some(0) => { if lambda.isPrimop { primop_lambdas.push(&item); } @@ -110,7 +117,6 @@ pub fn categorize(data: &Vec) -> FnCategories { } } _ => { - // # partially_applieds.push(&item); } } diff --git a/pesto/src/pasta.rs b/pesto/src/pasta.rs index e6abe8f..1bf8ad1 100644 --- a/pesto/src/pasta.rs +++ b/pesto/src/pasta.rs @@ -125,12 +125,10 @@ impl<'a> Lookups<'a> for Docs { && !i.docs.attr.content.as_ref().unwrap().is_empty() { Some(ContentSource { - content: i - .docs - .attr - .content - .as_ref() - .map(|inner| dedent(inner)), + content: i.docs.attr.content.as_ref().map(|inner| { + let fmt = dedent(inner); + fmt + }), source: Some(SourceOrigin { position: i.docs.attr.position.as_ref(), path: Some(&i.path), @@ -138,7 +136,6 @@ impl<'a> Lookups<'a> for Docs { }), }) } else { - // i.lambda_content() None } }) diff --git a/pesto/test_data/aliases/add.json b/pesto/test_data/aliases/add.json index 4b79f3d..cc415c0 100644 --- a/pesto/test_data/aliases/add.json +++ b/pesto/test_data/aliases/add.json @@ -13,6 +13,7 @@ "arity": 2, "content": "\n Return the sum of the numbers *e1* and *e2*.\n ", "experimental": false, + "countApplied": 0, "isPrimop": true, "name": "add", "position": null @@ -34,6 +35,7 @@ "arity": 2, "content": "\n Return the sum of the numbers *e1* and *e2*.\n ", "experimental": false, + "countApplied": 0, "isPrimop": true, "name": "add", "position": null diff --git a/pesto/test_data/aliases/escapeURL.expect b/pesto/test_data/aliases/escapeURL.expect new file mode 100644 index 0000000..f0e7b8b --- /dev/null +++ b/pesto/test_data/aliases/escapeURL.expect @@ -0,0 +1,54 @@ +[ + { + "aliases": [ + [ + "lib", + "escapeURL" + ] + ], + "path": [ + "lib", + "strings", + "escapeURL" + ] + }, + { + "aliases": [ + [ + "lib", + "strings", + "escapeURL" + ] + ], + "path": [ + "lib", + "escapeURL" + ] + }, + { + "aliases": [ + [ + "lib", + "escapeRegex" + ] + ], + "path": [ + "lib", + "strings", + "escapeRegex" + ] + }, + { + "aliases": [ + [ + "lib", + "strings", + "escapeRegex" + ] + ], + "path": [ + "lib", + "escapeRegex" + ] + } +] \ No newline at end of file diff --git a/pesto/test_data/aliases/escapeURL.json b/pesto/test_data/aliases/escapeURL.json new file mode 100644 index 0000000..0d4676c --- /dev/null +++ b/pesto/test_data/aliases/escapeURL.json @@ -0,0 +1,74 @@ +[ + { + "docs": { + "attr": { + "position": { + "column": 3, + "file": "/nix/store/mdy5n400sl4dn12kjkrkwnxqfbzahpld-nixpkgs-migrated/lib/strings.nix", + "line": 718 + } + }, + "lambda": { + "content": "\n Given string *s*, replace every occurrence of the strings in *from*\n with the corresponding string in *to*.\n\n The argument *to* is lazy, that is, it is only evaluated when its corresponding pattern in *from* is matched in the string *s*\n\n Example:\n\n ```nix\n builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\"\n ```\n\n evaluates to `\"fabir\"`.\n ", + "countApplied": 2, + "isPrimop": true, + "position": null + } + }, + "path": ["lib", "strings", "escapeURL"] + }, + { + "docs": { + "attr": { + "position": { + "column": 27, + "file": "/nix/store/mdy5n400sl4dn12kjkrkwnxqfbzahpld-nixpkgs-migrated/lib/default.nix", + "line": 98 + } + }, + "lambda": { + "content": "\n Given string *s*, replace every occurrence of the strings in *from*\n with the corresponding string in *to*.\n\n The argument *to* is lazy, that is, it is only evaluated when its corresponding pattern in *from* is matched in the string *s*\n\n Example:\n\n ```nix\n builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\"\n ```\n\n evaluates to `\"fabir\"`.\n ", + "countApplied": 2, + "isPrimop": true, + "position": null + } + }, + "path": ["lib", "escapeURL"] + }, + { + "docs": { + "attr": { + "position": { + "column": 3, + "file": "/nix/store/mdy5n400sl4dn12kjkrkwnxqfbzahpld-nixpkgs-migrated/lib/strings.nix", + "line": 902 + } + }, + "lambda": { + "content": "\n Given string *s*, replace every occurrence of the strings in *from*\n with the corresponding string in *to*.\n\n The argument *to* is lazy, that is, it is only evaluated when its corresponding pattern in *from* is matched in the string *s*\n\n Example:\n\n ```nix\n builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\"\n ```\n\n evaluates to `\"fabir\"`.\n ", + "countApplied": 2, + "isPrimop": true, + "position": null + } + }, + "path": ["lib", "strings", "escapeRegex"] + }, + { + "docs": { + "attr": { + "position": { + "column": 27, + "file": "/nix/store/mdy5n400sl4dn12kjkrkwnxqfbzahpld-nixpkgs-migrated/lib/default.nix", + "line": 98 + } + }, + "lambda": { + "content": "\n Given string *s*, replace every occurrence of the strings in *from*\n with the corresponding string in *to*.\n\n The argument *to* is lazy, that is, it is only evaluated when its corresponding pattern in *from* is matched in the string *s*\n\n Example:\n\n ```nix\n builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\"\n ```\n\n evaluates to `\"fabir\"`.\n ", + "countApplied": 2, + "isPrimop": true, + "position": null + } + }, + "path": ["lib", "escapeRegex"] + } +] diff --git a/pesto/test_data/aliases/foldl.json b/pesto/test_data/aliases/foldl.json index 7a72a98..097ee61 100644 --- a/pesto/test_data/aliases/foldl.json +++ b/pesto/test_data/aliases/foldl.json @@ -45,6 +45,7 @@ "lambda": { "args": ["op", "nul", "list"], "arity": 3, + "countApplied": 0, "content": "\n Reduce a list by applying a binary operator, from left to right,\n e.g. `foldl' op nul [x0 x1 x2 ...] : op (op (op nul x0) x1) x2)\n ...`. For example, `foldl' (x: y: x + y) 0 [1 2 3]` evaluates to 6.\n The return value of each application of `op` is evaluated immediately,\n even for intermediate values.\n ", "experimental": false, "isPrimop": true, diff --git a/pesto/test_data/content_format/foldl'.expect b/pesto/test_data/content_format/foldl'.expect new file mode 100644 index 0000000..f89e7df --- /dev/null +++ b/pesto/test_data/content_format/foldl'.expect @@ -0,0 +1,6 @@ +[ + { + "name": "lib.lists.foldl'", + "content": "\nReduce a list by applying a binary operator from left to right,\nstarting with an initial accumulator.\nBefore each application of the operator, the accumulator value is evaluated.\nThis behavior makes this function stricter than [`foldl`](#function-library-lib.lists.foldl).\nUnlike [`builtins.foldl'`](https://nixos.org/manual/nix/unstable/language/builtins.html#builtins-foldl'),\nthe initial accumulator argument is evaluated before the first iteration.\n A call like\n ```nix\n foldl' op acc₀ [ x₀ x₁ x₂ ... xₙ₋₁ xₙ ]\n ```\n is (denotationally) equivalent to the following,\n but with the added benefit that `foldl'` itself will never overflow the stack.\n ```nix\n let\n acc₁ = builtins.seq acc₀ (op acc₀ x₀ );\n acc₂ = builtins.seq acc₁ (op acc₁ x₁ );\n acc₃ = builtins.seq acc₂ (op acc₂ x₂ );\n ...\n accₙ = builtins.seq accₙ₋₁ (op accₙ₋₁ xₙ₋₁);\n accₙ₊₁ = builtins.seq accₙ (op accₙ xₙ );\n in\n accₙ₊₁\n # Or ignoring builtins.seq\n op (op (... (op (op (op acc₀ x₀) x₁) x₂) ...) xₙ₋₁) xₙ\n ```\n\n # Example\n\n ```nix\n foldl' (acc: x: acc + x) 0 [1 2 3]\n => 6\n ```\n\n # Type\n\n ```\n foldl' :: (acc -> x -> acc) -> acc -> [x] -> acc\n ```\n\n # Arguments\n\n - [op] The binary operation to run, where the two arguments are:\n\n1. `acc`: The current accumulator value: Either the initial one for the first iteration, or the result of the previous iteration\n2. `x`: The corresponding list element for this iteration\n - [acc] The initial accumulator value\n - [list] The list to fold\n\n" + } +] \ No newline at end of file diff --git a/pesto/test_data/content_format/foldl'.json b/pesto/test_data/content_format/foldl'.json new file mode 100644 index 0000000..5fdff67 --- /dev/null +++ b/pesto/test_data/content_format/foldl'.json @@ -0,0 +1,22 @@ +[ + { + "docs": { + "attr": { + "position": { + "column": 3, + "file": "/nix/store/knnp4h12pk09vfn18lrrrnh54zsvw3ba-source/lib/lists.nix", + "line": 198 + } + }, + "lambda": { + "isPrimop": false, + "position": { + "column": 5, + "file": "/nix/store/knnp4h12pk09vfn18lrrrnh54zsvw3ba-source/lib/lists.nix", + "line": 204 + } + } + }, + "path": ["lib", "lists", "foldl'"] + } +] diff --git a/website/.gitignore b/website/.gitignore index 922965b..858ed36 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -10,6 +10,10 @@ coverage src/models/data/* !src/models/data/index.ts +src/fonts/* +!src/fonts/index + + # nix .direnv/ diff --git a/website/flake-module.nix b/website/flake-module.nix index 4e4f776..43b4ddb 100644 --- a/website/flake-module.nix +++ b/website/flake-module.nix @@ -19,6 +19,7 @@ devShells.ui = pkgs.callPackage ./shell.nix { inherit pkgs hooks; inherit (base) fmod pkg; + inherit (self'.packages) data-json pasta-meta; }; }; } diff --git a/website/nix/pdefs.nix b/website/nix/pdefs.nix index ec8080f..237423e 100644 --- a/website/nix/pdefs.nix +++ b/website/nix/pdefs.nix @@ -1433,6 +1433,42 @@ version = "5.14.11"; }; }; + "@mui/material-nextjs" = { + "5.15.0" = { + fetchInfo = { + narHash = "sha256-4RvxiZFF+bL+mlttOzR5F/B4H7676KS2OOrNVVOqfVM="; + type = "tarball"; + url = "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-5.15.0.tgz"; + }; + ident = "@mui/material-nextjs"; + ltype = "file"; + peerInfo = { + "@emotion/cache" = { + descriptor = "^11.11.0"; + optional = true; + }; + "@emotion/server" = { + descriptor = "^11.11.0"; + optional = true; + }; + "@mui/material" = { + descriptor = "^5.0.0"; + }; + "@types/react" = { + descriptor = "^17.0.0 || ^18.0.0"; + optional = true; + }; + next = { + descriptor = "^13.0.0 || ^14.0.0"; + }; + react = { + descriptor = "^17.0.0 || ^18.0.0"; + }; + }; + treeInfo = { }; + version = "5.15.0"; + }; + }; "@mui/private-theming" = { "5.14.11" = { depInfo = { @@ -7228,6 +7264,19 @@ version = "1.0.0"; }; }; + hast = { + "1.0.0" = { + fetchInfo = { + narHash = "sha256-S+LfJO+BjiJg8xoRb0vjrWMDUuKxkdDGTS+251kcIaU="; + type = "tarball"; + url = "https://registry.npmjs.org/hast/-/hast-1.0.0.tgz"; + }; + ident = "hast"; + ltype = "file"; + treeInfo = { }; + version = "1.0.0"; + }; + }; hast-util-from-html = { "2.0.1" = { depInfo = { @@ -12209,16 +12258,16 @@ }; }; minisearch = { - "6.1.0" = { + "6.3.0" = { fetchInfo = { - narHash = "sha256-Yi/7KSMw1sSOI+H6Q2yoBUVj34gLq424e3Mxq522qD0="; + narHash = "sha256-/moYKxcAusTBnTb+oLc7lgDMjMLcnPFB9zwbzhRBM9U="; type = "tarball"; - url = "https://registry.npmjs.org/minisearch/-/minisearch-6.1.0.tgz"; + url = "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz"; }; ident = "minisearch"; ltype = "file"; treeInfo = { }; - version = "6.1.0"; + version = "6.3.0"; }; }; mri = { @@ -12473,6 +12522,11 @@ noogle = { "0.1.0" = { depInfo = { + "@emotion/cache" = { + descriptor = "^11.11.0"; + pin = "11.11.0"; + runtime = true; + }; "@emotion/react" = { descriptor = "^11.10.5"; pin = "11.11.1"; @@ -12513,6 +12567,11 @@ pin = "5.14.11"; runtime = true; }; + "@mui/material-nextjs" = { + descriptor = "^5.15.0"; + pin = "5.15.0"; + runtime = true; + }; "@next/mdx" = { descriptor = "^14.0.3"; pin = "14.0.3"; @@ -12564,14 +12623,24 @@ descriptor = "^14.0.3"; pin = "14.0.3"; }; + hast = { + descriptor = "^1.0.0"; + pin = "1.0.0"; + runtime = true; + }; + hast-util-to-string = { + descriptor = "^3.0.0"; + pin = "3.0.0"; + runtime = true; + }; "highlight.js" = { descriptor = "^11.7.0"; pin = "11.8.0"; runtime = true; }; minisearch = { - descriptor = "^6.0.1"; - pin = "6.1.0"; + descriptor = "^6.3.0"; + pin = "6.3.0"; runtime = true; }; next = { @@ -12614,6 +12683,11 @@ pin = "0.15.0"; runtime = true; }; + react-hot-toast = { + descriptor = "^2.4.1"; + pin = "2.4.1"; + runtime = true; + }; "react-mark.js" = { descriptor = "^9.0.7"; pin = "9.0.7"; @@ -12689,6 +12763,11 @@ pin = "11.0.4"; runtime = true; }; + unist-util-visit = { + descriptor = "^5.0.0"; + pin = "5.0.0"; + runtime = true; + }; usehooks-ts = { descriptor = "^2.9.1"; pin = "2.9.1"; @@ -13064,6 +13143,9 @@ "node_modules/@mui/material" = { key = "@mui/material/5.14.11"; }; + "node_modules/@mui/material-nextjs" = { + key = "@mui/material-nextjs/5.15.0"; + }; "node_modules/@mui/material/node_modules/clsx" = { key = "clsx/2.0.0"; }; @@ -13917,6 +13999,9 @@ dev = true; key = "has-tostringtag/1.0.0"; }; + "node_modules/hast" = { + key = "hast/1.0.0"; + }; "node_modules/hast-util-from-html" = { key = "hast-util-from-html/2.0.1"; }; @@ -14896,7 +14981,7 @@ key = "minimist/1.2.8"; }; "node_modules/minisearch" = { - key = "minisearch/6.1.0"; + key = "minisearch/6.3.0"; }; "node_modules/mri" = { key = "mri/1.2.0"; @@ -15073,6 +15158,9 @@ "node_modules/react-highlight/node_modules/highlight.js" = { key = "highlight.js/10.7.3"; }; + "node_modules/react-hot-toast" = { + key = "react-hot-toast/2.4.1"; + }; "node_modules/react-is" = { key = "react-is/16.13.1"; }; @@ -16324,6 +16412,33 @@ version = "0.15.0"; }; }; + react-hot-toast = { + "2.4.1" = { + depInfo = { + goober = { + descriptor = "^2.1.10"; + pin = "2.1.13"; + runtime = true; + }; + }; + fetchInfo = { + narHash = "sha256-seRTGGyQWjwU+PNqAU71f8sLus509310whSQ4xNKs4Q="; + type = "tarball"; + url = "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz"; + }; + ident = "react-hot-toast"; + ltype = "file"; + peerInfo = { + react = { + descriptor = ">=16"; + }; + react-dom = { + descriptor = ">=16"; + }; + }; + version = "2.4.1"; + }; + }; react-is = { "16.13.1" = { fetchInfo = { diff --git a/website/package-lock.json b/website/package-lock.json index 91ed32b..7c35568 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -8,6 +8,7 @@ "name": "noogle", "version": "0.1.0", "dependencies": { + "@emotion/cache": "^11.11.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fontsource/roboto": "^5.0.0", @@ -16,9 +17,12 @@ "@mdx-js/react": "^3.0.0", "@mui/icons-material": "^5.10.9", "@mui/material": "^5.10.13", + "@mui/material-nextjs": "^5.15.0", "@next/mdx": "^14.0.3", "@types/mdx": "^2.0.10", "@vcarl/remark-headings": "^0.1.0", + "hast": "^1.0.0", + "hast-util-to-string": "^3.0.0", "highlight.js": "^11.7.0", "minisearch": "^6.3.0", "next": "^14.0.3", @@ -29,6 +33,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-highlight": "^0.15.0", + "react-hot-toast": "^2.4.1", "react-mark.js": "^9.0.7", "react-markdown": "^9.0.0", "react-minisearch": "^6.0.2", @@ -44,6 +49,7 @@ "remark-stringify": "^11.0.0", "seedrandom": "^3.0.5", "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", "usehooks-ts": "^2.9.1" }, "devDependencies": { @@ -1783,6 +1789,37 @@ } } }, + "node_modules/@mui/material-nextjs": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-5.15.0.tgz", + "integrity": "sha512-UC3lnoRqJXoWUBeekqjxC4CpxMElz4D4bBCegOhrm80qGV1cP5TBJGjalqoSUq6HSl+COVv6u//AjjIMKCj/qA==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/server": "^11.11.0", + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "next": "^13.0.0 || ^14.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/cache": { + "optional": true + }, + "@emotion/server": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material/node_modules/clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -4690,6 +4727,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hast/-/hast-1.0.0.tgz", + "integrity": "sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==", + "deprecated": "Renamed to rehype" + }, "node_modules/hast-util-from-html": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", @@ -10143,6 +10186,21 @@ "node": "*" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -12913,6 +12971,12 @@ } } }, + "@mui/material-nextjs": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-5.15.0.tgz", + "integrity": "sha512-UC3lnoRqJXoWUBeekqjxC4CpxMElz4D4bBCegOhrm80qGV1cP5TBJGjalqoSUq6HSl+COVv6u//AjjIMKCj/qA==", + "requires": {} + }, "@mui/private-theming": { "version": "5.14.11", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.11.tgz", @@ -15006,6 +15070,11 @@ "has-symbols": "^1.0.2" } }, + "hast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hast/-/hast-1.0.0.tgz", + "integrity": "sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==" + }, "hast-util-from-html": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", @@ -18329,6 +18398,14 @@ } } }, + "react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "requires": { + "goober": "^2.1.10" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/website/package.json b/website/package.json index 8213f6b..295f2ac 100644 --- a/website/package.json +++ b/website/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@emotion/cache": "^11.11.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fontsource/roboto": "^5.0.0", @@ -17,9 +18,12 @@ "@mdx-js/react": "^3.0.0", "@mui/icons-material": "^5.10.9", "@mui/material": "^5.10.13", + "@mui/material-nextjs": "^5.15.0", "@next/mdx": "^14.0.3", "@types/mdx": "^2.0.10", "@vcarl/remark-headings": "^0.1.0", + "hast": "^1.0.0", + "hast-util-to-string": "^3.0.0", "highlight.js": "^11.7.0", "minisearch": "^6.3.0", "next": "^14.0.3", @@ -30,6 +34,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-highlight": "^0.15.0", + "react-hot-toast": "^2.4.1", "react-mark.js": "^9.0.7", "react-markdown": "^9.0.0", "react-minisearch": "^6.0.2", @@ -45,6 +50,7 @@ "remark-stringify": "^11.0.0", "seedrandom": "^3.0.5", "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", "usehooks-ts": "^2.9.1" }, "devDependencies": { diff --git a/website/public/google_logo.png b/website/public/google_logo.png new file mode 100644 index 0000000..1c73c16 Binary files /dev/null and b/website/public/google_logo.png differ diff --git a/website/shell.nix b/website/shell.nix index 9f12eec..2e636b0 100644 --- a/website/shell.nix +++ b/website/shell.nix @@ -1,9 +1,16 @@ -{ fmod, pkg, pkgs, hooks, ... }: +{ data-json, pasta-meta, fmod, pkg, pkgs, hooks, ... }: pkgs.mkShell { packages = [ fmod.config.floco.settings.nodePackage ]; shellHook = '' ${hooks.prepare "src/models/data"} + cp -f ${data-json} src/models/data/data.json + cp -f ${pasta-meta} src/models/data/meta.json + cp -rf ${pkgs.inter}/share/fonts/opentype/* src/fonts + cp -rf ${pkgs.fira-code}/share/fonts/truetype/* src/fonts + chmod -R +w src/models/data + chmod -R +w src/fonts + ID=${pkg.built.tree} currID=$(cat .floco/.node_modules_id 2> /dev/null) diff --git a/website/src/app/f/[...path]/layout.tsx b/website/src/app/f/[...path]/layout.tsx new file mode 100644 index 0000000..7244655 --- /dev/null +++ b/website/src/app/f/[...path]/layout.tsx @@ -0,0 +1,12 @@ +import { Header } from "@/components/layout/header"; +import { Container } from "@mui/material"; +import { ReactNode } from "react"; + +export default function SearchLayout({ children }: { children: ReactNode }) { + return ( + <> +
+ {children} + + ); +} diff --git a/website/src/app/f/[...path]/page.tsx b/website/src/app/f/[...path]/page.tsx new file mode 100644 index 0000000..545115a --- /dev/null +++ b/website/src/app/f/[...path]/page.tsx @@ -0,0 +1,230 @@ +import { HighlightBaseline } from "@/components/HighlightBaseline"; +import { ShareButton } from "@/components/ShareButton"; +import { BackButton } from "@/components/back"; +import { Doc, FilePosition, data } from "@/models/data"; +import { getPrimopDescription } from "@/models/primop"; +import { extractHeadings, mdxRenderOptions } from "@/utils"; +import { Edit } from "@mui/icons-material"; +import { Box, Button, Divider, Typography, Link, Chip } from "@mui/material"; +import { MDXRemote } from "next-mdx-remote/rsc"; + +// Important the key ("path") in the returned dict MUST match the dynamic path segment ([...path]) +export async function generateStaticParams(): Promise<{ path: string[] }[]> { + const paths = data.map((docItem) => { + return { + path: docItem.meta.path.map((s) => s), + }; + }); + return paths; +} + +const getSourcePosition = (baseUrl: string, position: FilePosition): string => { + const filename = position?.file.split("/").slice(4).join("/"); + const line = position?.line; + const column = position?.column; + let res = `${baseUrl}`; + if (filename && line && column) { + res += `/${filename}#L${line}:C${column}`; + } + return res; +}; + +interface TocProps { + mdxSource: string; +} + +const Toc = async (props: TocProps) => { + const { mdxSource } = props; + const headings = await extractHeadings(mdxSource); + + return ( + + Table of Contents + + {headings.map((h, idx) => ( + + + {h.value} + + + ))} + + + ); +}; + +// Multiple versions of this page will be statically generated +// using the `params` returned by `generateStaticParams` +export default async function Page(props: { params: { path: string[] } }) { + const { params } = props; + const item: Doc | undefined = data.find( + (item) => item.meta.path.join(".") === params.path.join(".") + ); + const mdxSource = item?.content?.content || ""; + const meta = item?.meta; + + return ( + <> + + + + + + + {item?.meta.title} + {meta?.is_primop && meta.count_applied == 0 && ( + <> + + {meta?.primop_meta?.experimental && ( + + )} + + )} + + + + + + + ( + // @ts-ignore + + ), + // @ts-ignore + h1: (p) => ( + // @ts-ignore + + ), + // @ts-ignore + h2: (p) => , + // @ts-ignore + h3: (p) => , + // @ts-ignore + h4: (p) => , + // @ts-ignore + h5: (p) => ( + // @ts-ignore + + ), + // @ts-ignore + h6: (p) => ( + // @ts-ignore + + ), + }} + /> + {meta?.content_meta?.position && ( + <> + + + + + + + )} + {!!meta?.aliases?.length && ( + <> + + + Noogle also knows + + + + Aliases + +
    + {meta?.aliases?.map((a) => ( +
  • + + {a.join(".")} + +
  • + ))} +
+ + )} +
+
+ + ); +} diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx index a5ad91c..34bb513 100644 --- a/website/src/app/layout.tsx +++ b/website/src/app/layout.tsx @@ -1,59 +1,57 @@ -"use client"; -import { AppState } from "@/components/appState"; -import { Layout } from "@/components/layout"; -import { CacheProvider, ThemeProvider } from "@emotion/react"; -import { CssBaseline, createTheme, useMediaQuery } from "@mui/material"; -import Head from "next/head"; -import { SnackbarProvider } from "notistack"; -import createEmotionCache from "../../createEmotionCache"; +import { CssBaseline } from "@mui/material"; import "../styles/globals.css"; -import { darkThemeOptions, lightThemeOptions } from "../styles/theme"; +import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter"; +import localFont from "next/font/local"; +import { ClientSideLayoutContext } from "@/components/ClientSideLayoutContext"; +import { Metadata } from "next"; -const clientSideEmotionCache = createEmotionCache(); -const lightTheme = createTheme(lightThemeOptions); -const darkTheme = createTheme(darkThemeOptions); +export const metadata: Metadata = { + title: "Noogle", + creator: "@hsjobeki", + abstract: "Nix and NixOS API Documentation", + robots: { index: true, notranslate: true, nocache: true }, + icons: "/favicon.png", +}; + +const inter = localFont({ + src: "../fonts/Inter-Regular.otf", + display: "swap", +}); +// /* noogle +// +// +// */ export default function RootLayout({ - // Layouts must accept a children prop. - // This will be populated with nested layouts or pages children, }: { children: React.ReactNode; }) { - const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)"); return ( - - - noogle - - - - - + + + {/* */} - + - - + + - - - {children} - - - - + {children} + + ); diff --git a/website/src/app/page.tsx b/website/src/app/page.tsx index 3e9957c..ee40395 100644 --- a/website/src/app/page.tsx +++ b/website/src/app/page.tsx @@ -1,15 +1,78 @@ -"use client"; -import NixFunctions from "@/components/NixFunctions"; -import { usePageContext } from "@/components/pageContext"; +import { FunctionOfTheDay } from "@/components/functionOfTheDay"; +import { LandingPageLayout } from "@/components/layout"; +import { SearchInput } from "@/components/searchInput"; +import { Box, Typography, Link } from "@mui/material"; + +import localFont from "next/font/local"; + +const orbitron = localFont({ + src: "../fonts/FiraCode-VF.ttf", + display: "swap", +}); export default function Home() { - const { pageState, setPageStateVariable } = usePageContext(); return ( - // - - // + + + + + N:: + + + o + + + o + + + g + + + + l + + + e + + + {` |>`} + + + + + + + + + + ); } diff --git a/website/src/app/q/layout.tsx b/website/src/app/q/layout.tsx new file mode 100644 index 0000000..bcc3976 --- /dev/null +++ b/website/src/app/q/layout.tsx @@ -0,0 +1,11 @@ +import { Header } from "@/components/layout/header"; +import { ReactNode } from "react"; + +export default function SearchLayout({ children }: { children: ReactNode }) { + return ( + <> +
+ {children} + + ); +} diff --git a/website/src/app/q/page.tsx b/website/src/app/q/page.tsx new file mode 100644 index 0000000..25c2b75 --- /dev/null +++ b/website/src/app/q/page.tsx @@ -0,0 +1,12 @@ +"use client"; +import { SearchResults } from "@/components/SearchResults"; +import { LinearProgress } from "@mui/material"; +import { Suspense } from "react"; + +export default function Q() { + return ( + }> + + + ); +} diff --git a/website/src/app/ref/[...id]/page.tsx b/website/src/app/ref/[...id]/page.tsx deleted file mode 100644 index cd40387..0000000 --- a/website/src/app/ref/[...id]/page.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { - docsDir, - extractHeadings, - getMdxMeta, - getMdxSource, - mdxRenderOptions, -} from "@/utils"; -import { Edit } from "@mui/icons-material"; -import { Box, Button, Typography } from "@mui/material"; -import fs from "fs"; -// import "highlight.js/styles/github-dark-dimmed.css"; -import "highlight.js/styles/github-dark.css"; -import { MDXRemote } from "next-mdx-remote/rsc"; -import Link from "next/link"; -import path from "path"; - -export async function generateStaticParams() { - const files = fs.readdirSync(docsDir, { - recursive: true, - withFileTypes: true, - encoding: "utf-8", - }); - const paths: { id: string[] }[] = files - .filter((f) => !f.isDirectory()) - .map((f) => { - const dirname = path.relative(docsDir, f.path); - const filename = path.parse(f.name).name; - return { - id: [...dirname.split("/"), filename], - }; - }); - return paths; -} - -interface TocProps { - mdxSource: Buffer; -} - -const Toc = async (props: TocProps) => { - const { mdxSource } = props; - const headings = await extractHeadings(mdxSource); - - return ( - - - Table of Contents - - {headings.map((h, idx) => ( - - - - ))} - - - - ); -}; - -// Multiple versions of this page will be statically generated -// using the `params` returned by `generateStaticParams` -export default async function Page(props: { params: { id: string[] } }) { - const { mdxSource } = await getMdxSource(props.params.id); - const meta = await getMdxMeta(props.params.id); - console.log("matter", meta.compiled.frontmatter); - const { frontmatter } = meta.compiled; - return ( - <> - - - - - {frontmatter.path ? frontmatter.path.join(".") : frontmatter.title} - - ( - // @ts-ignore - - ), - h2: (p) => ( - // @ts-ignore - - ), - h3: (p) => ( - // @ts-ignore - - ), - h4: (p) => ( - // @ts-ignore - - ), - h5: (p) => ( - // @ts-ignore - - ), - h6: (p) => ( - // @ts-ignore - - ), - }} - /> - - - - - ); -} diff --git a/website/src/app/ref/layout.tsx b/website/src/app/ref/layout.tsx deleted file mode 100644 index cfbb689..0000000 --- a/website/src/app/ref/layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { NavSidebar } from "@/components/NavSidebar"; -import { Box } from "@mui/material"; - -export default async function Page({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
- - - {children} - -
- ); -} diff --git a/website/src/components/ClientSideLayoutContext.tsx b/website/src/components/ClientSideLayoutContext.tsx new file mode 100644 index 0000000..d594de2 --- /dev/null +++ b/website/src/components/ClientSideLayoutContext.tsx @@ -0,0 +1,28 @@ +"use client"; +import { + CssBaseline, + ThemeProvider, + createTheme, + useMediaQuery, +} from "@mui/material"; +import { darkThemeOptions, lightThemeOptions } from "@/styles/theme"; +import { ReactNode } from "react"; +import { Toaster } from "react-hot-toast"; +const darkTheme = createTheme(darkThemeOptions); +const lightTheme = createTheme(lightThemeOptions); + +export const ClientSideLayoutContext = ({ + children, +}: { + children: ReactNode; +}) => { + const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)"); + return ( + + + + + {children} + + ); +}; diff --git a/website/src/components/Excerpt.tsx b/website/src/components/Excerpt.tsx new file mode 100644 index 0000000..5a4f011 --- /dev/null +++ b/website/src/components/Excerpt.tsx @@ -0,0 +1,57 @@ +import { Doc } from "@/models/data"; +import bash from "highlight.js/lib/languages/bash"; +import haskell from "highlight.js/lib/languages/haskell"; +import nix from "highlight.js/lib/languages/nix"; +import rehypeHighlight from "rehype-highlight"; + +import { rehypeExtractExcerpt } from "@/excerpt"; +import Markdown from "react-markdown"; +import { HighlightBaseline } from "./HighlightBaseline"; +import rehypeStringify from "rehype-stringify"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; +import { useEffect, useState } from "react"; + +const getExcerpt = async (content: string): Promise => { + const processor = unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeExtractExcerpt) + .use(rehypeStringify); + + const result = await processor.process(content); + return result.data["excerpt"] as string; +}; + +export const Excerpt = ({ meta, content }: Doc) => { + const mdxSource = content?.content || ""; + const [excerpt, setExcerpt] = useState(""); + useEffect(() => { + getExcerpt(mdxSource).then((r) => setExcerpt(r)); + }, [mdxSource]); + return ( + <> + + + {excerpt} + + + ); +}; diff --git a/website/src/components/HighlightBaseline.tsx b/website/src/components/HighlightBaseline.tsx new file mode 100644 index 0000000..c79eeb9 --- /dev/null +++ b/website/src/components/HighlightBaseline.tsx @@ -0,0 +1,17 @@ +"use client"; +import { useTheme } from "@mui/material"; +import { useEffect } from "react"; + +export const HighlightBaseline = () => { + const theme = useTheme(); + useEffect(() => { + if (theme.palette.mode === "dark") { + // @ts-ignore - don't check type of css module + import("highlight.js/styles/github-dark.css"); + } else { + // @ts-ignore - don't check type of css module + import("highlight.js/styles/github.css"); + } + }, [theme]); + return <>; +}; diff --git a/website/src/components/NixFunctions/index.ts b/website/src/components/NixFunctions/index.ts deleted file mode 100644 index 8c1bd31..0000000 --- a/website/src/components/NixFunctions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {NixFunctions as default} from "./nixFunctions"; \ No newline at end of file diff --git a/website/src/components/NixFunctions/nixFunctions.tsx b/website/src/components/NixFunctions/nixFunctions.tsx deleted file mode 100644 index fcdbd87..0000000 --- a/website/src/components/NixFunctions/nixFunctions.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; -import { Doc } from "@/models/data"; -import { PageState } from "@/models/internals"; -import { Box } from "@mui/system"; -import { useEffect } from "react"; -import { useMiniSearch } from "react-minisearch"; -import { BasicList, BasicListItem } from "../basicList"; -import FunctionItem from "../functionItem/functionItem"; -import { SetPageStateVariable } from "../pageContext"; -interface FunctionsProps { - pageState: PageState; - setPageStateVariable: SetPageStateVariable; -} - -// export const commonSearchOptions = { -// // allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match) -// fuzzy: 0.22, -// // prefer to show builtins first -// boostDocument: (id: string, term: string) => { -// let boost = 1; -// boost *= id.includes("builtins") ? 10 : 1; -// boost *= id.includes(term) ? 100 : 1; -// return boost; -// }, -// boost: { -// id: 1000, -// name: 100, -// category: 10, -// example: 0.5, -// fn_type: 10, -// description: 1, -// }, -// }; - -export function NixFunctions(props: FunctionsProps) { - const { pageState, setPageStateVariable } = props; - const { data, selected, term, filter } = pageState; - - const setSelected = setPageStateVariable("selected"); - - const minisearch = useMiniSearch(data, { - idField: "meta.title", - fields: ["meta.title", "content.content"], - // @ts-ignore - extractField: (document, fieldName) => { - // Access nested fields - return fieldName - .split(".") - .reduce( - (doc, key) => doc && doc[key], - document - ) as Document[keyof Document]; - }, - }); - - const { search, searchResults, rawResults } = minisearch; - //initial site-load is safe to call - useEffect(() => { - search(term); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filteredData = term ? searchResults || [] : data; - - const preRenderedItems: BasicListItem[] = filteredData.map((docItem: Doc) => { - const key = docItem.meta.title; - const matches = - rawResults?.find((r) => r.id === docItem.meta.title)?.terms || []; - return { - item: ( - setSelected(key) : undefined} - > - setSelected(null)} - /> - - ), - key, - }; - }); - - return ( - - - - ); -} diff --git a/website/src/components/SearchResults.tsx b/website/src/components/SearchResults.tsx new file mode 100644 index 0000000..bdc1cfa --- /dev/null +++ b/website/src/components/SearchResults.tsx @@ -0,0 +1,175 @@ +"use client"; +import { + Box, + Container, + Divider, + LinearProgress, + Link, + List, + ListItem, + ListItemText, + TablePagination, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import React, { useEffect, useMemo, useState } from "react"; + +import { useMiniSearch } from "react-minisearch"; + +import { Doc, data } from "@/models/data"; +import { EmptyRecordsPlaceholder } from "./emptyRecordsPlaceholder"; +import { useSearchParams } from "next/navigation"; +import { Excerpt } from "./Excerpt"; +import { useRouter } from "next/navigation"; + +export type BasicListItem = { + item: React.ReactNode; + key: string; +}; + +const useMobile = () => useMediaQuery(useTheme().breakpoints.down("md")); + +export function SearchResults() { + const params = useSearchParams(); + const router = useRouter(); + + const query = useMemo(() => new URLSearchParams(params), [params]); + + const page = +params.get("page")! || 1; + const itemsPerPage = +params.get("limit")! || 10; + const term = params.get("term") || ""; + + const isMobile = useMobile(); + + const [isLoading, setLoading] = useState(true); + const { search, searchResults } = useMiniSearch(data, { + idField: "meta.title", + fields: ["meta.title", "content.content", "bla.bu"], + // @ts-ignore + extractField: (document, fieldName) => { + // Access nested fields + // @ts-ignore + return fieldName.split(".").reduce( + // @ts-ignore + (doc, key) => doc && doc[key], + document + ) as Document[keyof Document]; + }, + }); + useEffect(() => { + search(term); + }, [term, search]); + + const handleChangeRowsPerPage = ( + event: React.ChangeEvent + ) => { + query.set("limit", event.target.value); + query.set("page", "1"); + router.push(`?${query.toString()}`); + }; + // useEffect(() => { + // if (searchResults !== null) { + // setLoading(false); + // } + // console.log({ searchResults }); + // }, [searchResults]); + // console.log({ isLoading }); + + const pageItems = useMemo(() => { + const items = searchResults || []; + const startIdx = (page - 1) * itemsPerPage; + const endIdx = page * itemsPerPage; + return items.slice(startIdx, endIdx); + }, [page, itemsPerPage, searchResults]); + + const handlePageChange = ( + event: React.MouseEvent | null, + value: number + ) => { + query.set("page", (value + 1).toString()); + router.push(`?${query.toString()}`); + }; + + return ( + + + {searchResults === null ? ( + + ) : ( + <> + + {searchResults?.length} Results + + + {pageItems.length ? ( + pageItems.map(({ meta, content }, idx) => ( + + + + {meta.title} + + } + secondary={} + /> + + + + )) + ) : ( + + + + )} + + + + )} + + + ); +} diff --git a/website/src/components/ShareButton.tsx b/website/src/components/ShareButton.tsx new file mode 100644 index 0000000..885d4c0 --- /dev/null +++ b/website/src/components/ShareButton.tsx @@ -0,0 +1,18 @@ +"use client"; +import { Share } from "@mui/icons-material"; +import { IconButton } from "@mui/material"; +import toast from "react-hot-toast"; + +export const ShareButton = () => { + const handleShare = () => { + const handle = window.location.href; + navigator.clipboard.writeText(handle); + toast.success("link copied to clipboard"); + }; + + return ( + handleShare()}> + + + ); +}; diff --git a/website/src/components/appState/appState.tsx b/website/src/components/appState/appState.tsx deleted file mode 100644 index 91da7b9..0000000 --- a/website/src/components/appState/appState.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; -import { InitialPageState, initialPageState } from "@/models/internals"; -import { ReadonlyURLSearchParams, useSearchParams } from "next/navigation"; -import React, { useEffect, useState } from "react"; - -import { PageState } from "@/models/internals"; -import { PageContextProvider } from "../pageContext"; - -const getInitialProps = async (query: ReadonlyURLSearchParams) => { - const initialProps = { ...initialPageState }; - query.forEach((value, key) => { - if (value && !Array.isArray(value)) { - try { - const parsedValue = JSON.parse(value) as never; - const initialValue = initialPageState[key as keyof InitialPageState]; - - if (!initialValue || typeof parsedValue === typeof initialValue) { - initialProps[key as keyof InitialPageState] = JSON.parse( - value - ) as never; - } else { - throw "Type of query param does not match the initial values type"; - } - } catch (error) { - console.error("Invalid query:", { key, value, error }); - } - } - }); - const FOTD = Object.entries(query).length === 0; - - return { - props: { - ...initialProps, - FOTD, - }, - }; -}; - -interface AppStateProps { - children: React.ReactNode; -} -export function AppState(props: AppStateProps) { - const { children } = props; - const params = useSearchParams(); - const [initialProps, setInitialProps] = useState(null); - useEffect(() => { - if (initialProps === null) { - getInitialProps(params).then((r) => { - const { props } = r; - console.info("Url Query changed\n\nUpdating pageState with delta:", { - props, - }); - setInitialProps((curr) => ({ ...curr, ...props })); - }); - } - }, [initialProps, params]); - return ( - <> - {initialProps && ( - - {children} - - )} - - ); -} diff --git a/website/src/components/appState/index.ts b/website/src/components/appState/index.ts deleted file mode 100644 index 0be4524..0000000 --- a/website/src/components/appState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AppState } from "./appState"; \ No newline at end of file diff --git a/website/src/components/back.tsx b/website/src/components/back.tsx new file mode 100644 index 0000000..105da88 --- /dev/null +++ b/website/src/components/back.tsx @@ -0,0 +1,13 @@ +"use client"; +import { ChevronLeft } from "@mui/icons-material"; +import { IconButton } from "@mui/material"; +import { useRouter } from "next/navigation"; + +export const BackButton = () => { + const router = useRouter(); + return ( + router.back()}> + + + ); +}; diff --git a/website/src/components/basicList/basicList.tsx b/website/src/components/basicList/basicList.tsx deleted file mode 100644 index bbf150c..0000000 --- a/website/src/components/basicList/basicList.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { BasicDataViewProps } from "@/types/basicDataView"; -import { Box, List, ListItem, Stack, TablePagination } from "@mui/material"; -import React, { useMemo } from "react"; -import { SearchInput } from "../searchInput"; - -import { ViewMode } from "@/models/internals"; -import { UseMiniSearch } from "react-minisearch"; -import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder"; -import { FunctionOfTheDay } from "../functionOfTheDay"; -import { useMobile } from "../layout/layout"; -import { usePageContext } from "../pageContext"; -import { Filter } from "../searchInput/searchInput"; -import { Doc } from "@/models/data"; - -export type BasicListItem = { - item: React.ReactNode; - key: string; -}; -export type BasicListProps = BasicDataViewProps & { - selected?: string | null; - minisearch: UseMiniSearch; -}; - -export function BasicList(props: BasicListProps) { - const { items, minisearch } = props; - const { search, suggestions, autoSuggest, clearSuggestions } = minisearch; - const { pageState, setPageStateVariable, resetQueries } = usePageContext(); - const isMobile = useMobile(); - const { page, itemsPerPage, FOTD: showFunctionOfTheDay, data } = pageState; - - const setViewMode = setPageStateVariable("viewMode"); - const setPage = setPageStateVariable("page"); - const setTerm = setPageStateVariable("term"); - const setFilter = setPageStateVariable("filter"); - const setItemsPerPage = setPageStateVariable("itemsPerPage"); - - const handleChangeRowsPerPage = ( - event: React.ChangeEvent - ) => { - setItemsPerPage(parseInt(event.target.value, 10)); - setPage(1); - }; - const pageItems = useMemo(() => { - const startIdx = (page - 1) * itemsPerPage; - const endIdx = page * itemsPerPage; - return items.slice(startIdx, endIdx); - }, [page, items, itemsPerPage]); - - const handlePageChange = ( - event: React.MouseEvent | null, - value: number - ) => { - setPage(value + 1); - }; - const handleClear = () => { - resetQueries(); - }; - - const handleFilter = (filter: Filter | ((curr: Filter) => Filter)) => { - setFilter(filter); - setPage(1); - }; - - const handleSearch = (term: string) => { - setTerm(term); - search(term); - setPage(1); - }; - - return ( - - - {showFunctionOfTheDay && ( - { - setViewMode("browse"); - }} - /> - )} - - {!showFunctionOfTheDay && ( - - {items.length ? ( - pageItems.map(({ item, key }, idx) => ( - - - {item} - - - )) - ) : ( - - - - )} - - )} - {!showFunctionOfTheDay && ( - - )} - - ); -} diff --git a/website/src/components/basicList/index.tsx b/website/src/components/basicList/index.tsx deleted file mode 100644 index 7de2dee..0000000 --- a/website/src/components/basicList/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { BasicList } from "./basicList"; -export type { BasicListProps, BasicListItem } from "./basicList"; diff --git a/website/src/components/fun.tsx b/website/src/components/fun.tsx deleted file mode 100644 index b8a713b..0000000 --- a/website/src/components/fun.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { pseudoRandomIntInclusive } from "@/client"; -import { useMemo, useState } from "react"; -import Markdown from "react-markdown"; - -interface FunctionOfTheDayProps { - allPaths: { id: string[] }[]; -} -export const FunctionOfTheDay = (props: FunctionOfTheDayProps) => { - const { allPaths } = props; - - const todaysIdx = useMemo( - () => pseudoRandomIntInclusive(0, allPaths.length - 1), - [allPaths.length] - ); - const [idx] = useState(todaysIdx); - - // redirect(`ref/${allPaths[idx].id.join("/")}`); - // const setFunctionOfTheDay = () => { - // setIdx(todaysIdx); - // }; - - return ( - <> -
{idx}
-
{allPaths[idx].id.join("/")}
- {allPaths[idx].id.join("\n")} - - ); -}; diff --git a/website/src/components/functionItem/functionItem.tsx b/website/src/components/functionItem/functionItem.tsx deleted file mode 100644 index 1397366..0000000 --- a/website/src/components/functionItem/functionItem.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Doc } from "@/models/data"; -import ManageSearchIcon from "@mui/icons-material/ManageSearch"; -import ShareIcon from "@mui/icons-material/Share"; -import { - IconButton, - ListItemText, - Paper, - Stack, - Toolbar, - Tooltip, - Typography, -} from "@mui/material"; -import { useSnackbar } from "notistack"; -import { useMemo, useState } from "react"; -import { Marker } from "react-mark.js"; -import { Preview } from "../preview/preview"; - -interface FunctionItemProps { - selected: boolean; - name: String; - docItem: Doc; - handleClose: () => void; - markWords: string[]; -} - -export default function FunctionItem(props: FunctionItemProps) { - const { docItem, selected, handleClose, markWords } = props; - // const { fn_type, category, description, id } = docItem; - const { content, meta } = docItem; - const [mark, setMark] = useState(false); - const { enqueueSnackbar } = useSnackbar(); - - const descriptionPreview = useMemo(() => { - const getFirstWords = (s: string) => { - const indexOfDot = s.indexOf("."); - if (indexOfDot) { - return s.slice(0, indexOfDot + 1); - } - return s.split(" ").filter(Boolean).slice(0, 10).join(" "); - }; - if (content?.content) { - return getFirstWords(content?.content); - } - return ""; - }, [content]); - - const handleShare = () => { - const handle = window.location.href; - navigator.clipboard.writeText(handle); - enqueueSnackbar("link copied to clipboard", { variant: "default" }); - }; - - // const normalId: string = useMemo(() => id, [id]); - - return ( - - - {!selected && ( - <> - - - - {`Types cannot be detected yet. Work with us on migrating this feature.`} - - - )} - {selected && ( - <> - - - - - {Boolean(markWords.length) && ( - - setMark((s) => !s)}> - - - - )} - - - - - - - - )} - - - ); -} diff --git a/website/src/components/functionOfTheDay/functionOfTheDay.tsx b/website/src/components/functionOfTheDay/functionOfTheDay.tsx index 54e4691..3690cf0 100644 --- a/website/src/components/functionOfTheDay/functionOfTheDay.tsx +++ b/website/src/components/functionOfTheDay/functionOfTheDay.tsx @@ -6,15 +6,12 @@ import { CardContent, CardHeader, Divider, - IconButton, useTheme, } from "@mui/material"; - -import { MetaData } from "@/models/nix"; -import ClearIcon from "@mui/icons-material/Clear"; import { useMemo, useState } from "react"; import seedrandom from "seedrandom"; import { Preview } from "../preview/preview"; +import { Doc, data } from "@/models/data"; const date = new Date(); @@ -37,22 +34,17 @@ function getRandomIntInclusive(min: number, max: number, config?: Config) { return Math.floor(randomNumber * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive } -interface FunctionOfTheDayProps { - handleClose: () => void; - data: MetaData; -} -export const FunctionOfTheDay = (props: FunctionOfTheDayProps) => { - const { handleClose, data } = props; +export const FunctionOfTheDay = () => { const { palette: { info, error }, } = useTheme(); const todaysIdx = useMemo( () => getRandomIntInclusive(0, data.length - 1), - [data.length] + [] ); const [idx, setIdx] = useState(todaysIdx); - const selectedFunction = useMemo(() => data.at(idx) as Doc, [idx, data]); + const selectedFunction = useMemo(() => data.at(idx) as Doc, [idx]); const setNext = () => { setIdx((curr) => { @@ -79,65 +71,59 @@ export const FunctionOfTheDay = (props: FunctionOfTheDayProps) => { }; return ( - <> - + + + + + - handleClose()}> - - - } - /> - - } /> - - setPrev()} + disabled={idx === 0} + sx={{ width: "100%" }} > - - - - - - - - - + Prev + + + + + + + + ); }; diff --git a/website/src/components/layout/Background.tsx b/website/src/components/layout/Background.tsx new file mode 100644 index 0000000..d85f5b6 --- /dev/null +++ b/website/src/components/layout/Background.tsx @@ -0,0 +1,21 @@ +"use client"; +import { Box, useTheme } from "@mui/material"; +import { ReactNode } from "react"; + +export const Background = ({ children }: { children: ReactNode }) => { + const theme = useTheme(); + return ( + + {children} + + ); +}; diff --git a/website/src/components/layout/header.tsx b/website/src/components/layout/header.tsx new file mode 100644 index 0000000..969c6da --- /dev/null +++ b/website/src/components/layout/header.tsx @@ -0,0 +1,71 @@ +"use client"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import { Box, IconButton, Link, Tooltip, useTheme } from "@mui/material"; +import { SearchInput } from "../searchInput"; +import { Menu } from "@mui/icons-material"; + +export const Header = () => { + const theme = useTheme(); + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/website/src/components/layout/index.ts b/website/src/components/layout/index.ts index 453b1d5..c1e0413 100644 --- a/website/src/components/layout/index.ts +++ b/website/src/components/layout/index.ts @@ -1 +1 @@ -export { Layout } from "./layout" \ No newline at end of file +export { LandingPageLayout } from "./layout"; diff --git a/website/src/components/layout/layout.tsx b/website/src/components/layout/layout.tsx index 3ea605e..d5e6739 100644 --- a/website/src/components/layout/layout.tsx +++ b/website/src/components/layout/layout.tsx @@ -1,6 +1,3 @@ -import nixSnowflake from "@/assets/nix-snowflake.svg"; -import nixWhite from "@/assets/white.svg"; -import GitHubIcon from "@mui/icons-material/GitHub"; import { Box, Container, @@ -8,130 +5,87 @@ import { Link, Tooltip, Typography, - useMediaQuery, - useTheme, } from "@mui/material"; -import { Image } from "../image"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import PublicIcon from "@mui/icons-material/Public"; +import { Background } from "./Background"; + export interface LayoutProps { children: React.ReactNode; } -export const useMobile = () => useMediaQuery(useTheme().breakpoints.down("md")); +const SocialIcons = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; -export function Layout(props: LayoutProps) { +export function LandingPageLayout(props: LayoutProps) { const { children } = props; - const theme = useTheme(); return ( - -
- - - - - - nix-logo - - {`noog\u03BBe`} - - - - - - - - - -
+ +
- + {children}
Powered by{" "} {" "} OceanSprint - + {" "} + © 2023 Noogle. All rights reserved. + + Noogle is an independent website and is not affiliated with or + endorsed by Google Inc. The use of the term Noogle is solely for the + purpose of branding and does not imply any connection with Google or + its products. +
-
+ ); } diff --git a/website/src/components/markdownPreview/MarkdownPreview.tsx b/website/src/components/markdownPreview/MarkdownPreview.tsx index 9c679ad..0e755f3 100644 --- a/website/src/components/markdownPreview/MarkdownPreview.tsx +++ b/website/src/components/markdownPreview/MarkdownPreview.tsx @@ -1,23 +1,41 @@ +"use client"; +import { useTheme } from "@mui/material"; import nix from "highlight.js/lib/languages/nix"; -import "highlight.js/styles/github-dark.css"; +import { useEffect } from "react"; import ReactMarkdown from "react-markdown"; import rehypeHighlight from "rehype-highlight"; +import { HighlightBaseline } from "../HighlightBaseline"; + interface MarkdownPreviewProps { description: string; } export const MarkdownPreview = (props: MarkdownPreviewProps) => { const { description } = props; + const theme = useTheme(); + useEffect(() => { + if (theme.palette.mode === "dark") { + // @ts-ignore - don't check type of css module + import("highlight.js/styles/github-dark.css"); + } else { + // @ts-ignore - don't check type of css module + import("highlight.js/styles/github.css"); + } + }, [theme]); + return ( - - {description} - + <> + + + {description} + + ); }; diff --git a/website/src/components/pageContext/index.ts b/website/src/components/pageContext/index.ts deleted file mode 100644 index a587345..0000000 --- a/website/src/components/pageContext/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { PageContextProvider, PageContext, usePageContext} from "./pageContext"; - -export type { SetPageStateVariable } from "./pageContext"; \ No newline at end of file diff --git a/website/src/components/pageContext/pageContext.tsx b/website/src/components/pageContext/pageContext.tsx deleted file mode 100644 index 72e195d..0000000 --- a/website/src/components/pageContext/pageContext.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; -import { - InitialPageState, - PageState, - initialPageState, -} from "@/models/internals"; -import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useEffect, useState } from "react"; - -type PageContextType = { - pageState: PageState; - setPageState: React.Dispatch>; - setPageStateVariable: SetPageStateVariable; - resetQueries: () => void; -}; - -const initialState = { ...initialPageState, FOTD: true }; -export const PageContext = React.createContext({ - pageState: initialState, - setPageState: () => {}, - resetQueries: () => {}, - setPageStateVariable: function a() { - return () => {}; - }, -}); - -interface PageContextProviderProps { - children: React.ReactNode; - pageProps: PageState; -} - -export type SetPageStateVariable = ( - field: keyof InitialPageState -) => (value: React.SetStateAction | T) => void; - -const paramList: Omit[] = [ - "filter", - "selected", - "term", - "page", - "itemsPerPage", -]; -const isQueryParam = (key: keyof PageState) => paramList.includes(key); -const isInitial = (key: keyof PageState, value: PageState[keyof PageState]) => - JSON.stringify(initialState[key]) === JSON.stringify(value); - -const updateRouter = (router: AppRouterInstance, state: PageState) => { - const searchParams = new URLSearchParams(Array.from([])); - - const query = Object.entries(state) - .filter(([k]) => isQueryParam(k as keyof PageState)) - .filter(([k, v]) => !isInitial(k as keyof PageState, v)) - .reduce((acc, [k, v]) => { - return { ...acc, [k]: JSON.stringify(v) }; - }, {}); - Object.entries(query).forEach(([key, value]) => { - searchParams.set(key, value as string); - }); - const queryString = searchParams.toString(); - - console.log({ query, queryString }); - router.push(`?${queryString}`); -}; - -export const PageContextProvider = (props: PageContextProviderProps) => { - const router = useRouter(); - const queryParams = useSearchParams(); - const { children, pageProps } = props; - const [pageState, setPageState] = useState(pageProps); - const { term, filter, viewMode } = pageState; - - function setPageStateVariable(field: keyof InitialPageState) { - return function (value: React.SetStateAction | T) { - if (typeof value !== "function") { - setPageState((curr) => { - const newState = { ...curr, [field]: value }; - updateRouter(router, newState); - return newState; - }); - } else { - const setter = value as Function; - setPageState((curr) => { - const newValue = setter(curr[field]); - const newState = { ...curr, [field]: newValue }; - updateRouter(router, newState); - return { ...curr, [field]: newValue }; - }); - } - }; - } - - function resetQueries() { - console.log({ queryParams }); - if (queryParams.size !== 0) { - router.push("?"); - } - setPageState((curr) => ({ ...curr, ...initialPageState })); - } - - useEffect(() => { - setPageState((c) => ({ - ...c, - FOTD: - viewMode === "explore" && - filter.to === "any" && - filter.from === "any" && - term === "", - })); - }, [viewMode, filter, term]); - - return ( - - {children} - - ); -}; - -export const usePageContext = () => React.useContext(PageContext); diff --git a/website/src/components/preview/preview.tsx b/website/src/components/preview/preview.tsx index 872f6ee..b25279d 100644 --- a/website/src/components/preview/preview.tsx +++ b/website/src/components/preview/preview.tsx @@ -1,48 +1,23 @@ import { Doc } from "@/models/data"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import InputIcon from "@mui/icons-material/Input"; -import LocalLibraryIcon from "@mui/icons-material/LocalLibrary"; import { Box, - Container, - IconButton, + Link, List, ListItem, - ListItemIcon, ListItemText, - Link as MuiLink, - Tooltip, Typography, - useTheme, } from "@mui/material"; import React from "react"; import { MarkdownPreview } from "../markdownPreview"; +import { getPrimopDescription } from "@/models/primop"; interface PreviewProps { docItem: Doc; - closeComponent?: React.ReactNode; - handleClose?: () => void; } export const Preview = (props: PreviewProps) => { - const { docItem, handleClose, closeComponent = undefined } = props; - // const { name, description, category, example, fn_type, id, line } = docItem; + const { docItem } = props; const { content, meta } = docItem; - const theme = useTheme(); - - // const normalId: string = useMemo(() => id, [id]); - - // const prefix = category.split(/([\/.])/gm).at(4) || "builtins"; - // const libName = category - // .match(/(?:[a-zA-Z]*)\.nix/gm)?.[0] - // ?.replace(".nix", ""); - // const sanitizedName = name.replace("'", "-prime"); - // const libDocsRef = `https://nixos.org/manual/nixpkgs/stable/#function-library-lib.${libName}.${sanitizedName}`; - // const builtinsDocsRef = `https://nixos.org/manual/nix/stable/language/builtins.html#builtins-${name}`; - const getPrimopDescription = (meta: Doc["meta"]["primop_meta"]) => { - const args = meta?.args?.map((a) => `__${a}__`) || []; - return `takes ${meta?.arity} arguments: ${args.join(", ")} \n`; - }; return ( { width: "100%", }} > - - {`${meta.title}`} - - {closeComponent || ( - - handleClose?.()} - > - - - - )} - - {/* {prefix !== "builtins" && id.includes("lib.") && ( - - - {`short form: lib.${name}`} + + + {`${meta.title}`} - - )} */} + +
- - - - - - - { width: "100%", px: 0, }} - primaryTypographyProps={{ - color: "text.secondary", - fontSize: 14, - }} - secondaryTypographyProps={{ - color: "text.primary", - fontSize: "1rem", - component: "div", - }} - // primary={ - // !id.includes("builtins") ? ( - // - // - // {"github:NixOS/nixpkgs/" + category.replace("./", "")} - // - // - // ) : ( - // "github:NixOS/nix/" + category.replace("./", "") - // ) - // } - secondary={ - - {meta?.is_primop && ( + {meta?.is_primop && meta.primop_meta && ( @@ -154,56 +71,15 @@ export const Preview = (props: PreviewProps) => { {!!content?.content && ( )} - + {!content?.content && ( + + Not documented yet. Contribute now! + + )} + } /> - - - - {/* */} - - {/* */} - - - - // ) : ( - // "no type provided yet." - // ) - } - primary="function signature " - /> - ); diff --git a/website/src/components/search.tsx b/website/src/components/search.tsx index 6d74a00..69587cf 100644 --- a/website/src/components/search.tsx +++ b/website/src/components/search.tsx @@ -37,7 +37,6 @@ export default function SearchPage() { } } - console.log("result", { results, query }); return (
void; - handleClear: () => void; - handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void; placeholder: string; - suggestions: Suggestion[]; - autoSuggest: (query: string, options?: SearchOptions) => void; - clearSuggestions: () => void; } export function SearchInput(props: SearchInputProps) { - const { - handleSearch, - placeholder, - handleFilter, - handleClear, - suggestions, - autoSuggest, - clearSuggestions, - } = props; - const { pageState } = usePageContext(); - const { filter, term } = pageState; - const [_term, _setTerm] = useState(term); + const { placeholder } = props; + const router = useRouter(); - const handleSubmit = React.useRef((input: string) => { - handleSearch(input); - }).current; + const [term, setTerm] = useState(""); - const debouncedSubmit = useMemo( - () => debounce(handleSubmit, 500), - [handleSubmit] - ); + const handleSubmit = (input: string) => { + router.push(`/q?term=${input}`); + }; const _handleClear = () => { - _setTerm(""); - handleClear(); - clearSuggestions(); + setTerm(""); + // clearSuggestions(); }; const handleType = ( e: React.ChangeEvent ) => { - _setTerm(e.target.value); - autoSuggest(e.target.value, { - fuzzy: 0.25, - fields: ["meta.title", "content.content"], - }); - debouncedSubmit(e.target.value); + setTerm(e.target.value); + // autoSuggest(e.target.value, { + // fuzzy: 0.25, + // fields: ["meta.title", "content.content"], + // }); }; - const autoCompleteOptions = useMemo(() => { - const options = suggestions - .slice(0, 5) - .map((s) => s.terms) - .flat(); - const sorted = options.sort((a, b) => -b.localeCompare(a)); - return [...new Set(sorted)]; - }, [suggestions]); + // @ts-ignore + // const autoCompleteOptions = useMemo(() => { + // const options = suggestions + // .slice(0, 5) + // .map((s) => s.terms) + // .flat(); + // const sorted = options.sort((a, b) => -b.localeCompare(a)); + // return [...new Set(sorted)]; + // }, [suggestions]); return ( - <> - { + e.preventDefault(); + handleSubmit(term); + }} + > + { + if (reason === "reset") { + handleSubmit(value); + } + }} + options={data.map((e) => e.meta.title)} + sx={{ width: "100%" }} + onChange={(e, value) => { + handleType({ + target: { value: value || "" }, + } as React.ChangeEvent); + }} + value={term} + renderInput={(params) => { + return ( + handleType(e)} + placeholder={placeholder} + {...params} + {...params.InputProps} + endAdornment={undefined} + /> + ); + }} + /> + + + + + { - e.preventDefault(); - handleSubmit(term); }} + aria-label="search-button" > - - - - - { - handleType({ - target: { value: value || "" }, - } as React.ChangeEvent); - }} - value={_term} - renderInput={(params) => { - return ( - // - handleType(e)} - {...params} - {...params.InputProps} - endAdornment={undefined} - /> - ); - }} - /> - - handleSearch(_term)} - > - - - - - - - - - Type search is temporarily unavailable. Due to ongoing RFC-145 - integration - - - {/* - { - handleFilter((curr) => ({ ...curr, from: value as NixType })); - }} - options={nixTypes.map((v) => ({ value: v, label: v }))} - /> - - - - - - - - { - handleFilter((curr) => ({ ...curr, to: value as NixType })); - }} - options={nixTypes.map((v) => ({ value: v, label: v }))} - /> - */} - - - + + + ); } diff --git a/website/src/components/themeRegistry.tsx b/website/src/components/themeRegistry.tsx index 02fa75d..4b4baf5 100644 --- a/website/src/components/themeRegistry.tsx +++ b/website/src/components/themeRegistry.tsx @@ -1,4 +1,3 @@ -// app/ThemeRegistry.tsx "use client"; import { darkThemeOptions, lightThemeOptions } from "@/styles/theme"; import createCache from "@emotion/cache"; diff --git a/website/src/excerpt.ts b/website/src/excerpt.ts new file mode 100644 index 0000000..c72fe83 --- /dev/null +++ b/website/src/excerpt.ts @@ -0,0 +1,77 @@ +import type { Root } from "hast"; +import { toString as hastToString } from "hast-util-to-string"; +import type { Plugin } from "unified"; +import { EXIT, visit } from "unist-util-visit"; + +export interface RehypeExtractExcerptOptions { + /** The var name of the vFile.data export. defaults to `excerpt` */ + name?: string; + + /** The character length to truncate the excerpt. defaults to 140 */ + maxLength?: number; + + /** The ellipsis to add to the excerpt. defaults to `...` */ + ellipsis?: string; + + /** Truncate the excerpt at word boundary. defaults to `true` */ + wordBoundaries?: boolean; + + /** The HTML tag name for the excerpt content. defaults to `p` */ + tagName?: string; +} + +const defaults: RehypeExtractExcerptOptions = { + name: "excerpt", + maxLength: 140, + ellipsis: "...", + wordBoundaries: true, + tagName: "p", +}; + +const rehypeExtractExcerpt: Plugin<[RehypeExtractExcerptOptions?], Root> = ( + userOptions?: RehypeExtractExcerptOptions +) => { + const options = { ...defaults, ...userOptions }; + + function truncateExcerpt( + str: string, + maxLength: number, + ellipsis: string, + wordBoundaries: boolean + ): string { + if (str.length > maxLength) { + if (wordBoundaries) { + return `${str.slice( + 0, + str.lastIndexOf(" ", maxLength - 1) + )}${ellipsis}`; + } + return `${str.slice(0, maxLength)}${ellipsis}`; + } + return str; + } + + return (tree, vfile) => { + const excerpt: string[] = []; + + visit(tree, "element", (node) => { + if (node.tagName !== options.tagName) { + return; + } + + excerpt.push( + truncateExcerpt( + hastToString(node), + options.maxLength!, + options.ellipsis!, + options.wordBoundaries! + ) + ); + + return EXIT; + }); + vfile.data[options.name!] = excerpt[0]; + }; +}; + +export { rehypeExtractExcerpt }; diff --git a/website/src/fonts/index b/website/src/fonts/index new file mode 100644 index 0000000..e69de29 diff --git a/website/src/models/data/index.ts b/website/src/models/data/index.ts index d4584a6..9777fce 100644 --- a/website/src/models/data/index.ts +++ b/website/src/models/data/index.ts @@ -1,10 +1,55 @@ // import nixTrivialBuilders from "./build_support.json" assert { type: "json" }; // import nixBuiltins from "./builtins.json" assert { type: "json" }; // import nixLibs from "./lib.json" assert { type: "json" }; -import all from "./json.json" assert { type: "json" }; +import all from "./data.json" assert { type: "json" }; +import builtinTypes from "./builtins.types.json" assert { type: "json" }; -export type Doc = (typeof all)[number]; +export type FilePosition = { + file: string; + line: number; + column: number; +}; + +export type PositionType = "Lambda" | "Attribute"; +export type SourceOrigin = { + position?: FilePosition; + path?: ValuePath; + pos_type?: PositionType; +}; + +export type PrimopMatter = { + name?: string; + args?: string[]; + experimental?: boolean; + arity?: number; +}; +export type ValuePath = string[]; + +export type DocMeta = { + title: string; + path: ValuePath; + aliases?: ValuePath[]; + is_primop?: boolean; + primop_meta?: PrimopMatter; + attr_position?: FilePosition; + lambda_position?: FilePosition; + count_applied?: number; + content_meta?: SourceOrigin; +}; +export type ContentSource = { + content?: string; + source?: SourceOrigin; +}; + +export type Doc = { + meta: DocMeta; + content?: ContentSource; +}; + +// export const data = all.sort((a, b) => +// a.meta.title.localeCompare(b.meta.title) +// ) as Doc[]; + +export const data: Doc[] = all as unknown as Doc[]; +export { builtinTypes }; -export const data = all.sort((a, b) => - a.meta.title.localeCompare(b.meta.title) -); diff --git a/website/src/models/primop.ts b/website/src/models/primop.ts new file mode 100644 index 0000000..d603683 --- /dev/null +++ b/website/src/models/primop.ts @@ -0,0 +1,8 @@ +import { PrimopMatter } from "./data"; + +export const getPrimopDescription = (meta: PrimopMatter) => { + const args = meta?.args?.map((a) => `__${a}__`) || []; + return !meta?.arity + ? "" + : `Takes __${meta?.arity}__ arguments\n\n ${args.join(", ")} \n`; +}; diff --git a/website/src/styles/globals.css b/website/src/styles/globals.css index 117dc67..2edc36c 100644 --- a/website/src/styles/globals.css +++ b/website/src/styles/globals.css @@ -2,14 +2,14 @@ html, body { padding: 0; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + font-family: inherit; + scroll-behavior: smooth; } -a { +/* a { color: inherit; text-decoration: none; -} +} */ * { box-sizing: border-box; @@ -23,4 +23,29 @@ mark.noogle-marker { /* :rgb(106, 213, 65); */ color: inherit; background-color: #88DD67; +} + + +code { + /* padding: 0.3rem; */ + background-color: #f0f1f2; +} +pre { + /* padding: 0.3rem; */ + background-color: #f0f1f2; +} +code.hljs { + background-color: #f0f1f2; +} + +@media (prefers-color-scheme: dark) { + code { + background-color: #0d1117; + } + pre { + background-color: #0d1117; + } + code.hljs { + background-color: #0d1117; + } } \ No newline at end of file diff --git a/website/src/styles/theme/common.ts b/website/src/styles/theme/common.ts new file mode 100644 index 0000000..06a8dc6 --- /dev/null +++ b/website/src/styles/theme/common.ts @@ -0,0 +1,18 @@ +import { ThemeOptions } from "@mui/material/styles"; + +const commonOptions: Partial = { + typography: { + fontFamily: "inherit", + h1: { + fontSize: "2.9rem", + }, + h2: { + fontSize: "2.6rem", + }, + h3: { + fontSize: "2.3rem", + }, + }, +}; + +export { commonOptions }; diff --git a/website/src/styles/theme/darkThemeOptions.ts b/website/src/styles/theme/darkThemeOptions.ts index 2fb784d..ffd5985 100644 --- a/website/src/styles/theme/darkThemeOptions.ts +++ b/website/src/styles/theme/darkThemeOptions.ts @@ -1,17 +1,19 @@ import { ThemeOptions } from "@mui/material/styles"; +import { commonOptions } from "./common"; const darkThemeOptions: ThemeOptions = { + ...commonOptions, palette: { mode: "dark", background: { - paper: "#0f192c" + paper: "#0f192c", }, primary: { - main: "#6586c8" + main: "#6586c8", }, secondary: { - main: "#6ad541" - } + main: "#6ad541", + }, }, }; diff --git a/website/src/styles/theme/lightThemeOptions.ts b/website/src/styles/theme/lightThemeOptions.ts index cb4b85c..4ccb082 100644 --- a/website/src/styles/theme/lightThemeOptions.ts +++ b/website/src/styles/theme/lightThemeOptions.ts @@ -1,18 +1,18 @@ import { ThemeOptions } from "@mui/material/styles"; - +import { commonOptions } from "./common"; const lightThemeOptions: ThemeOptions = { + ...commonOptions, palette: { mode: "light", primary: { - main: "#6586c8" - + main: "#6586c8", }, secondary: { - main: "#6ad541" + main: "#6ad541", }, background: { - default: "#fafaff" - } + default: "#fafaff", + }, }, }; diff --git a/website/src/utils.ts b/website/src/utils.ts index 7a68f39..7203727 100644 --- a/website/src/utils.ts +++ b/website/src/utils.ts @@ -2,7 +2,6 @@ import fs from "fs"; import bash from "highlight.js/lib/languages/bash"; import haskell from "highlight.js/lib/languages/haskell"; import nix from "highlight.js/lib/languages/nix"; -import "highlight.js/styles/github-dark-dimmed.css"; import { SerializeOptions } from "next-mdx-remote/dist/types"; import { CompileMDXResult, compileMDX } from "next-mdx-remote/rsc"; import { parse, serialize } from "parse5"; @@ -15,6 +14,7 @@ import rehypeStringify from "rehype-stringify"; import remarkHeadingId from "remark-heading-id"; import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; + import { unified } from "unified"; /** @@ -111,7 +111,7 @@ type Heading = { id: string; }; -export const extractHeadings = async (content: Buffer): Promise => { +export const extractHeadings = async (content: string): Promise => { const processor = unified() .use(remarkParse) .use(remarkHeadingId)