mirror of
https://github.com/hsjobeki/noogle.git
synced 2024-08-16 16:20:45 +03:00
redesign with subpages
This commit is contained in:
parent
26e9d0e7a3
commit
f637df2d2d
10
codemod/.envrc
Normal file
10
codemod/.envrc
Normal file
@ -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 ''
|
2
codemod/.gitignore
vendored
Normal file
2
codemod/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
result*
|
221
codemod/Cargo.lock
generated
Normal file
221
codemod/Cargo.lock
generated
Normal file
@ -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"
|
16
codemod/Cargo.toml
Normal file
16
codemod/Cargo.toml
Normal file
@ -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"
|
64
codemod/README.md
Normal file
64
codemod/README.md
Normal file
@ -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`
|
45
codemod/flake-module.nix
Normal file
45
codemod/flake-module.nix
Normal file
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
473
codemod/src/main.rs
Normal file
473
codemod/src/main.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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<SingleArg>),
|
||||
}
|
||||
|
||||
///
|
||||
fn handle_indentation(raw: &str) -> Option<String> {
|
||||
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<String> {
|
||||
// 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<Argument> {
|
||||
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<String> {
|
||||
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<String> = 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<String> = 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<String> {
|
||||
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<SyntaxNode> {
|
||||
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<String> = 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 <dirPath>");
|
||||
}
|
||||
}
|
||||
|
||||
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<SyntaxNode> = 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.");
|
||||
}
|
||||
}
|
59
codemod/test/args.nix
Normal file
59
codemod/test/args.nix
Normal file
@ -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);
|
||||
}
|
17
codemod/test/args/default.nix
Normal file
17
codemod/test/args/default.nix
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
/*
|
||||
Header
|
||||
|
||||
Example:
|
||||
assertMsg false "nope"
|
||||
|
||||
Type:
|
||||
assertMsg :: Bool -> String -> Bool
|
||||
|
||||
*/
|
||||
stuff =
|
||||
# a arg
|
||||
a:
|
||||
# b arg
|
||||
b: a;
|
||||
}
|
36
codemod/test/nested/sample.nix
Normal file
36
codemod/test/nested/sample.nix
Normal file
@ -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;
|
||||
}
|
22
codemod/test/simple.nix
Normal file
22
codemod/test/simple.nix
Normal file
@ -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
|
17
flake.lock
17
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"
|
||||
}
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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; };
|
||||
|
@ -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.<nested>)
|
||||
# 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 =
|
||||
|
@ -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.
|
||||
|
@ -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<Docs>) -> 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<Docs>) -> FnCategories {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// #
|
||||
partially_applieds.push(&item);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
|
54
pesto/test_data/aliases/escapeURL.expect
Normal file
54
pesto/test_data/aliases/escapeURL.expect
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
74
pesto/test_data/aliases/escapeURL.json
Normal file
74
pesto/test_data/aliases/escapeURL.json
Normal file
@ -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"]
|
||||
}
|
||||
]
|
@ -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,
|
||||
|
6
pesto/test_data/content_format/foldl'.expect
Normal file
6
pesto/test_data/content_format/foldl'.expect
Normal file
@ -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"
|
||||
}
|
||||
]
|
22
pesto/test_data/content_format/foldl'.json
Normal file
22
pesto/test_data/content_format/foldl'.json
Normal file
@ -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'"]
|
||||
}
|
||||
]
|
4
website/.gitignore
vendored
4
website/.gitignore
vendored
@ -10,6 +10,10 @@ coverage
|
||||
src/models/data/*
|
||||
!src/models/data/index.ts
|
||||
|
||||
src/fonts/*
|
||||
!src/fonts/index
|
||||
|
||||
|
||||
# nix
|
||||
.direnv/
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
devShells.ui = pkgs.callPackage ./shell.nix {
|
||||
inherit pkgs hooks;
|
||||
inherit (base) fmod pkg;
|
||||
inherit (self'.packages) data-json pasta-meta;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
77
website/package-lock.json
generated
77
website/package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
BIN
website/public/google_logo.png
Normal file
BIN
website/public/google_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -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)
|
||||
|
||||
|
12
website/src/app/f/[...path]/layout.tsx
Normal file
12
website/src/app/f/[...path]/layout.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Header />
|
||||
<Container maxWidth="lg">{children}</Container>
|
||||
</>
|
||||
);
|
||||
}
|
230
website/src/app/f/[...path]/page.tsx
Normal file
230
website/src/app/f/[...path]/page.tsx
Normal file
@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
display: {
|
||||
xs: "none",
|
||||
lg: "block",
|
||||
},
|
||||
position: "fixed",
|
||||
top: "6rem",
|
||||
right: "1.8em",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1">Table of Contents</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{headings.map((h, idx) => (
|
||||
<Link key={idx} href={`#${h.id}`}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
justifyContent: "start",
|
||||
textTransform: "none",
|
||||
color: "text.secondary",
|
||||
pl: (h.level - 1) * 2 + 1,
|
||||
py: 0.5,
|
||||
}}
|
||||
>
|
||||
{h.value}
|
||||
</Typography>
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<Toc mdxSource={mdxSource} />
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "100vw",
|
||||
overflow: "hidden",
|
||||
p: { xs: 1, md: 2 },
|
||||
bgcolor: "background.paper",
|
||||
}}
|
||||
>
|
||||
<HighlightBaseline />
|
||||
<Box>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Typography
|
||||
variant="h2"
|
||||
component={"h1"}
|
||||
sx={{ marginRight: "auto" }}
|
||||
>
|
||||
<BackButton /> {item?.meta.title}
|
||||
{meta?.is_primop && meta.count_applied == 0 && (
|
||||
<>
|
||||
<Chip
|
||||
label="Primop"
|
||||
color="primary"
|
||||
sx={{ ml: 2, maxWidth: "10rem" }}
|
||||
/>
|
||||
{meta?.primop_meta?.experimental && (
|
||||
<Chip
|
||||
label={"Experimental"}
|
||||
color="warning"
|
||||
sx={{ ml: 2, maxWidth: "10rem" }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
<ShareButton />
|
||||
</Box>
|
||||
|
||||
<Divider flexItem sx={{ mt: 2 }} />
|
||||
|
||||
<MDXRemote
|
||||
options={{
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: mdxRenderOptions,
|
||||
}}
|
||||
source={
|
||||
meta?.is_primop && meta?.primop_meta
|
||||
? getPrimopDescription(meta.primop_meta) + mdxSource
|
||||
: mdxSource
|
||||
}
|
||||
components={{
|
||||
a: (p) => (
|
||||
// @ts-ignore
|
||||
<Box
|
||||
sx={{
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
component="a"
|
||||
{...p}
|
||||
/>
|
||||
),
|
||||
// @ts-ignore
|
||||
h1: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="h3" component={"h2"} {...p} />
|
||||
),
|
||||
// @ts-ignore
|
||||
h2: (p) => <Typography variant="h4" component={"h3"} {...p} />,
|
||||
// @ts-ignore
|
||||
h3: (p) => <Typography variant="h5" component={"h4"} {...p} />,
|
||||
// @ts-ignore
|
||||
h4: (p) => <Typography variant="h6" component={"h5"} {...p} />,
|
||||
// @ts-ignore
|
||||
h5: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="subtitle1" component={"h6"} {...p} />
|
||||
),
|
||||
// @ts-ignore
|
||||
h6: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="subtitle2" component={"h6"} {...p} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{meta?.content_meta?.position && (
|
||||
<>
|
||||
<Typography variant="subtitle2" sx={{ color: "text.secondary" }}>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={getSourcePosition(
|
||||
"https://github.com/hsjobeki/nixpkgs/tree/migrate-doc-comments",
|
||||
meta.content_meta.position
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
sx={{ textTransform: "none", my: 1, placeSelf: "start" }}
|
||||
startIcon={<Edit />}
|
||||
>
|
||||
Edit source
|
||||
</Button>
|
||||
</Link>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{!!meta?.aliases?.length && (
|
||||
<>
|
||||
<Divider flexItem />
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component={"h3"}
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
alignSelf: "center",
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
Noogle also knows
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component={"h3"}>
|
||||
Aliases
|
||||
</Typography>
|
||||
<ul>
|
||||
{meta?.aliases?.map((a) => (
|
||||
<li key={a.join(".")}>
|
||||
<Link
|
||||
href={`/f/${a.join("/")}`}
|
||||
// sx={{ color: "primary.main" }}
|
||||
>
|
||||
{a.join(".")}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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",
|
||||
});
|
||||
// /* <title>noogle</title>
|
||||
// <meta charSet="utf-8" />
|
||||
// <meta
|
||||
// name="description"
|
||||
// content="Search nix functions. Search functions within the nix ecosystem based on type, name, description, example, category and more."
|
||||
// />
|
||||
// <meta /> */
|
||||
|
||||
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 (
|
||||
<html lang="en">
|
||||
<Head>
|
||||
<title>noogle</title>
|
||||
<meta charSet="utf-8" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Search nix functions. Search functions within the nix ecosystem based on type, name, description, example, category and more."
|
||||
/>
|
||||
<meta />
|
||||
<meta name="robots" content="all" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<html lang="en" className={inter.className}>
|
||||
<head>
|
||||
{/* <link rel="icon" href="/favicon.png" /> */}
|
||||
<link
|
||||
rel="search"
|
||||
type="application/opensearchdescription+xml"
|
||||
title="Search nix function on noogle"
|
||||
href="/search.xml"
|
||||
></link>
|
||||
</Head>
|
||||
</head>
|
||||
<body>
|
||||
<CacheProvider value={clientSideEmotionCache}>
|
||||
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
|
||||
<AppRouterCacheProvider
|
||||
options={{
|
||||
enableCssLayer: true,
|
||||
}}
|
||||
>
|
||||
<ClientSideLayoutContext>
|
||||
<CssBaseline />
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
maxSnack={1}
|
||||
>
|
||||
<AppState>
|
||||
<Layout>{children}</Layout>
|
||||
</AppState>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
{children}
|
||||
</ClientSideLayoutContext>
|
||||
</AppRouterCacheProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -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 (
|
||||
// <PageContextProvider>
|
||||
<NixFunctions
|
||||
pageState={pageState}
|
||||
setPageStateVariable={setPageStateVariable}
|
||||
/>
|
||||
// </PageContextProvider>
|
||||
<LandingPageLayout>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Link href="/" underline="none">
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={orbitron.className}
|
||||
sx={{
|
||||
mt: 10,
|
||||
mb: 4,
|
||||
fontSize: "4.5rem",
|
||||
fontVariantLigatures: "normal",
|
||||
}}
|
||||
>
|
||||
<Box component="span">N::</Box>
|
||||
|
||||
<Box component="span" sx={{ color: "error.main" }}>
|
||||
o
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: "warning.light" }}>
|
||||
o
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: "primary.main" }}>
|
||||
g
|
||||
</Box>
|
||||
|
||||
<Box component="span" sx={{ color: "success.light" }}>
|
||||
l
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: "error.main" }}>
|
||||
e
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: "error.main" }}>
|
||||
{` |>`}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Link>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
py: 1,
|
||||
px: 2,
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: 2,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
>
|
||||
<SearchInput placeholder="search nix functions" />
|
||||
</Box>
|
||||
<FunctionOfTheDay />
|
||||
</Box>
|
||||
</LandingPageLayout>
|
||||
);
|
||||
}
|
||||
|
11
website/src/app/q/layout.tsx
Normal file
11
website/src/app/q/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function SearchLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
12
website/src/app/q/page.tsx
Normal file
12
website/src/app/q/page.tsx
Normal file
@ -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 (
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<SearchResults />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
top: 70,
|
||||
right: 0,
|
||||
position: "absolute",
|
||||
order: 2,
|
||||
width: "19rem",
|
||||
px: 2,
|
||||
mr: 8,
|
||||
}}
|
||||
component={"aside"}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 4,
|
||||
pl: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1">Table of Contents</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{headings.map((h, idx) => (
|
||||
<Link key={idx} href={`#${h.id}`}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="text"
|
||||
sx={{
|
||||
justifyContent: "start",
|
||||
textTransform: "none",
|
||||
color: "text.secondary",
|
||||
pl: (h.level - 1) * 2,
|
||||
}}
|
||||
>
|
||||
{h.value}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<Toc mdxSource={mdxSource} />
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Box sx={{ order: 1, width: "60rem", marginInline: "auto", py: 2 }}>
|
||||
<Typography variant="h2" component={"h1"}>
|
||||
{frontmatter.path ? frontmatter.path.join(".") : frontmatter.title}
|
||||
</Typography>
|
||||
<MDXRemote
|
||||
options={{
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: mdxRenderOptions,
|
||||
}}
|
||||
source={mdxSource}
|
||||
components={{
|
||||
h1: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="h3" component={"h2"} {...p} />
|
||||
),
|
||||
h2: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="h4" component={"h3"} {...p} />
|
||||
),
|
||||
h3: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="h5" component={"h4"} {...p} />
|
||||
),
|
||||
h4: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="h6" component={"h5"} {...p} />
|
||||
),
|
||||
h5: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="subtitle1" component={"h6"} {...p} />
|
||||
),
|
||||
h6: (p) => (
|
||||
// @ts-ignore
|
||||
<Typography variant="subtitle2" component={"h6"} {...p} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button sx={{ textTransform: "none", my: 4 }} startIcon={<Edit />} >
|
||||
Edit source {frontmatter.}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import { NavSidebar } from "@/components/NavSidebar";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default async function Page({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<aside
|
||||
style={{
|
||||
position: "fixed",
|
||||
height: "100vh",
|
||||
overflowY: "scroll",
|
||||
overflowX: "hidden",
|
||||
left: 0,
|
||||
width: "19rem",
|
||||
}}
|
||||
>
|
||||
<NavSidebar />
|
||||
</aside>
|
||||
<Box
|
||||
sx={{
|
||||
ml: "25em",
|
||||
px: 2,
|
||||
py: 4,
|
||||
w: "100%",
|
||||
bgcolor: "background.paper",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
28
website/src/components/ClientSideLayoutContext.tsx
Normal file
28
website/src/components/ClientSideLayoutContext.tsx
Normal file
@ -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 (
|
||||
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
|
||||
<CssBaseline />
|
||||
<Toaster />
|
||||
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
57
website/src/components/Excerpt.tsx
Normal file
57
website/src/components/Excerpt.tsx
Normal file
@ -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<string> => {
|
||||
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<string>("");
|
||||
useEffect(() => {
|
||||
getExcerpt(mdxSource).then((r) => setExcerpt(r));
|
||||
}, [mdxSource]);
|
||||
return (
|
||||
<>
|
||||
<HighlightBaseline />
|
||||
<Markdown
|
||||
components={{
|
||||
h1: "h2",
|
||||
h2: "h3",
|
||||
h3: "h4",
|
||||
h4: "h5",
|
||||
}}
|
||||
rehypePlugins={[
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
detect: true,
|
||||
languages: { nix, haskell, bash, default: nix },
|
||||
},
|
||||
],
|
||||
]}
|
||||
>
|
||||
{excerpt}
|
||||
</Markdown>
|
||||
</>
|
||||
);
|
||||
};
|
17
website/src/components/HighlightBaseline.tsx
Normal file
17
website/src/components/HighlightBaseline.tsx
Normal file
@ -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 <></>;
|
||||
};
|
@ -1 +0,0 @@
|
||||
export {NixFunctions as default} from "./nixFunctions";
|
@ -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<string | null>("selected");
|
||||
|
||||
const minisearch = useMiniSearch<Doc>(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: (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
onClick={!(selected === key) ? () => setSelected(key) : undefined}
|
||||
>
|
||||
<FunctionItem
|
||||
markWords={matches}
|
||||
name={docItem.meta.title}
|
||||
docItem={docItem}
|
||||
selected={selected === key}
|
||||
handleClose={() => setSelected(null)}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
key,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ ml: { xs: 0, md: 2 } }}>
|
||||
<BasicList items={preRenderedItems} minisearch={minisearch} />
|
||||
</Box>
|
||||
);
|
||||
}
|
175
website/src/components/SearchResults.tsx
Normal file
175
website/src/components/SearchResults.tsx
Normal file
@ -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<boolean>(true);
|
||||
const { search, searchResults } = useMiniSearch<Doc>(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<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
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<HTMLButtonElement> | null,
|
||||
value: number
|
||||
) => {
|
||||
query.set("page", (value + 1).toString());
|
||||
router.push(`?${query.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
{searchResults === null ? (
|
||||
<LinearProgress sx={{ my: 2 }} />
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: "text.secondary", p: 1 }}
|
||||
>
|
||||
{searchResults?.length} Results
|
||||
</Typography>
|
||||
<List aria-label="basic-list" sx={{ pt: 0, width: "100%" }}>
|
||||
{pageItems.length ? (
|
||||
pageItems.map(({ meta, content }, idx) => (
|
||||
<Box key={`${idx}`}>
|
||||
<ListItem sx={{ px: 0 }} aria-label={`item-${idx}`}>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
variant: "h5",
|
||||
component: "h2",
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
variant: "body1",
|
||||
}}
|
||||
primary={
|
||||
<Link href={`f/${meta.path.join("/")}`}>
|
||||
{meta.title}
|
||||
</Link>
|
||||
}
|
||||
secondary={<Excerpt meta={meta} content={content} />}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider
|
||||
flexItem
|
||||
orientation="horizontal"
|
||||
sx={{ p: 1, mx: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<EmptyRecordsPlaceholder
|
||||
CardProps={{
|
||||
sx: { backgroundColor: "inherit" },
|
||||
}}
|
||||
title={"No search results found"}
|
||||
subtitle={
|
||||
"Maybe the function does not exist or is not documented."
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</List>
|
||||
<TablePagination
|
||||
component={"div"}
|
||||
sx={{ display: "flex", justifyContent: "center", mt: 1, mb: 10 }}
|
||||
count={searchResults?.length || -1}
|
||||
color="primary"
|
||||
page={page - 1}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={itemsPerPage}
|
||||
labelRowsPerPage={"per Page"}
|
||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
showFirstButton={!isMobile}
|
||||
showLastButton={!isMobile}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
18
website/src/components/ShareButton.tsx
Normal file
18
website/src/components/ShareButton.tsx
Normal file
@ -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 (
|
||||
<IconButton onClick={() => handleShare()}>
|
||||
<Share />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
@ -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<PageState | null>(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 && (
|
||||
<PageContextProvider pageProps={initialProps}>
|
||||
{children}
|
||||
</PageContextProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { AppState } from "./appState";
|
13
website/src/components/back.tsx
Normal file
13
website/src/components/back.tsx
Normal file
@ -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 (
|
||||
<IconButton onClick={() => router.back()}>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
@ -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<Doc>;
|
||||
};
|
||||
|
||||
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>("viewMode");
|
||||
const setPage = setPageStateVariable<number>("page");
|
||||
const setTerm = setPageStateVariable<string>("term");
|
||||
const setFilter = setPageStateVariable<Filter>("filter");
|
||||
const setItemsPerPage = setPageStateVariable<number>("itemsPerPage");
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
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<HTMLButtonElement> | 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 (
|
||||
<Stack>
|
||||
<SearchInput
|
||||
handleFilter={handleFilter}
|
||||
handleClear={handleClear}
|
||||
placeholder="search nix functions"
|
||||
handleSearch={handleSearch}
|
||||
suggestions={suggestions || []}
|
||||
autoSuggest={autoSuggest}
|
||||
clearSuggestions={clearSuggestions}
|
||||
/>
|
||||
{showFunctionOfTheDay && (
|
||||
<FunctionOfTheDay
|
||||
data={data}
|
||||
handleClose={() => {
|
||||
setViewMode("browse");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showFunctionOfTheDay && (
|
||||
<List aria-label="basic-list" sx={{ pt: 0 }}>
|
||||
{items.length ? (
|
||||
pageItems.map(({ item, key }, idx) => (
|
||||
<Box key={`${key}-${idx}`}>
|
||||
<ListItem sx={{ px: 0 }} key={key} aria-label={`item-${key}`}>
|
||||
{item}
|
||||
</ListItem>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<EmptyRecordsPlaceholder
|
||||
CardProps={{
|
||||
sx: { backgroundColor: "inherit" },
|
||||
}}
|
||||
title={"No search results found"}
|
||||
subtitle={
|
||||
"Maybe the function does not exist or is not documented."
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
{!showFunctionOfTheDay && (
|
||||
<TablePagination
|
||||
component={"div"}
|
||||
sx={{ display: "flex", justifyContent: "center", mt: 1, mb: 10 }}
|
||||
count={items.length}
|
||||
color="primary"
|
||||
page={page - 1}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={itemsPerPage}
|
||||
labelRowsPerPage={"per Page"}
|
||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
showFirstButton={!isMobile}
|
||||
showLastButton={!isMobile}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export { BasicList } from "./basicList";
|
||||
export type { BasicListProps, BasicListItem } from "./basicList";
|
@ -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<number>(todaysIdx);
|
||||
|
||||
// redirect(`ref/${allPaths[idx].id.join("/")}`);
|
||||
// const setFunctionOfTheDay = () => {
|
||||
// setIdx(todaysIdx);
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{idx}</div>
|
||||
<div>{allPaths[idx].id.join("/")}</div>
|
||||
<Markdown>{allPaths[idx].id.join("\n")}</Markdown>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<Boolean>(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 (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
cursor: !selected ? "pointer" : "default",
|
||||
display: "flex",
|
||||
justifyContent: "left",
|
||||
px: selected ? 0 : { xs: 0.8, md: 2 },
|
||||
pt: 1,
|
||||
mb: 0,
|
||||
color: selected ? "primary.main" : undefined,
|
||||
borderColor: selected ? "primary.light" : "none",
|
||||
borderWidth: 1,
|
||||
borderStyle: selected ? "solid" : "none",
|
||||
"&:hover": !selected
|
||||
? {
|
||||
backgroundColor: "action.hover",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<Stack sx={{ width: "100%" }}>
|
||||
{!selected && (
|
||||
<>
|
||||
<ListItemText primary={`${meta.title}`} secondary={meta.title} />
|
||||
<ListItemText secondary={descriptionPreview} />
|
||||
<Typography
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{`Types cannot be detected yet. Work with us on migrating this feature.`}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{selected && (
|
||||
<>
|
||||
<Marker
|
||||
mark={mark ? markWords : []}
|
||||
options={{ className: "noogle-marker" }}
|
||||
>
|
||||
<Preview docItem={docItem} handleClose={handleClose} />
|
||||
</Marker>
|
||||
<Toolbar
|
||||
sx={{
|
||||
justifyContent: "end",
|
||||
}}
|
||||
>
|
||||
{Boolean(markWords.length) && (
|
||||
<Tooltip title={`${mark ? "Hide" : "Show"} matches`}>
|
||||
<IconButton onClick={() => setMark((s) => !s)}>
|
||||
<ManageSearchIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Share">
|
||||
<IconButton onClick={handleShare}>
|
||||
<ShareIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Toolbar>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
@ -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<number>(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 (
|
||||
<>
|
||||
<Card
|
||||
elevation={0}
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
width: "100%",
|
||||
my: 5,
|
||||
borderImageSlice: 1,
|
||||
borderImageSource:
|
||||
idx === todaysIdx
|
||||
? `linear-gradient(to left, ${info.light},${error.main})`
|
||||
: `linear-gradient(to left, ${info.light},${info.dark})`,
|
||||
borderWidth: 4,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
title={
|
||||
idx === todaysIdx
|
||||
? "Function of the day"
|
||||
: "Did you know the function?"
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Preview docItem={selectedFunction} />
|
||||
</CardContent>
|
||||
<CardActions
|
||||
sx={{
|
||||
my: 5,
|
||||
borderImageSlice: 1,
|
||||
borderImageSource:
|
||||
idx === todaysIdx
|
||||
? `linear-gradient(to left, ${info.light},${error.main})`
|
||||
: `linear-gradient(to left, ${info.light},${info.dark})`,
|
||||
borderWidth: 4,
|
||||
borderStyle: "solid",
|
||||
display: "flex",
|
||||
justifyContent: "space-evenly",
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
title={
|
||||
idx === todaysIdx
|
||||
? "Function of the day"
|
||||
: "Did you know the function?"
|
||||
}
|
||||
action={
|
||||
<IconButton onClick={() => handleClose()}>
|
||||
<ClearIcon fontSize="large" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Preview docItem={selectedFunction} closeComponent={<></>} />
|
||||
</CardContent>
|
||||
<CardActions
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-evenly",
|
||||
}}
|
||||
<Button
|
||||
onClick={() => setPrev()}
|
||||
disabled={idx === 0}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setPrev()}
|
||||
disabled={idx === 0}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Divider flexItem orientation="vertical" />
|
||||
<Button sx={{ width: "100%" }} onClick={() => setRandom()}>
|
||||
Random
|
||||
</Button>
|
||||
<Button sx={{ width: "100%" }} onClick={() => setFunctionOfTheDay()}>
|
||||
Todays function
|
||||
</Button>
|
||||
<Divider flexItem orientation="vertical" />
|
||||
<Button
|
||||
sx={{ width: "100%" }}
|
||||
onClick={() => setNext()}
|
||||
disabled={idx === data.length - 1}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</>
|
||||
Prev
|
||||
</Button>
|
||||
<Divider flexItem orientation="vertical" />
|
||||
<Button sx={{ width: "100%" }} onClick={() => setRandom()}>
|
||||
Random
|
||||
</Button>
|
||||
<Button sx={{ width: "100%" }} onClick={() => setFunctionOfTheDay()}>
|
||||
Todays function
|
||||
</Button>
|
||||
<Divider flexItem orientation="vertical" />
|
||||
<Button
|
||||
sx={{ width: "100%" }}
|
||||
onClick={() => setNext()}
|
||||
disabled={idx === data.length - 1}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
21
website/src/components/layout/Background.tsx
Normal file
21
website/src/components/layout/Background.tsx
Normal file
@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "scroll",
|
||||
bgcolor:
|
||||
theme.palette.mode === "light" ? "rgb(242, 248, 253)" : "#070c16",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
71
website/src/components/layout/header.tsx
Normal file
71
website/src/components/layout/header.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
width: "100%",
|
||||
py: 1.2,
|
||||
zIndex: 1,
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light"
|
||||
? "primary.main"
|
||||
: "background.default",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "0.3fr 0.4fr 0.3fr",
|
||||
alignContent: "center",
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomColor: "#232223",
|
||||
borderBottomWidth: "1px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ justifySelf: "start", color: "primary.contrastText" }}>
|
||||
<IconButton color="inherit">
|
||||
<Menu />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
justifySelf: "center",
|
||||
width: "100%",
|
||||
minWidth: "20rem",
|
||||
bgcolor: "background.paper",
|
||||
px: 2,
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<SearchInput placeholder="search nix functions" />
|
||||
</Box>
|
||||
<Box sx={{ justifySelf: "end", mr: 2 }}>
|
||||
<Link
|
||||
href="https://github.com/nix-community/noogle"
|
||||
target="_blank"
|
||||
sx={{ color: "primary.contrastText" }}
|
||||
>
|
||||
<Tooltip title="Github">
|
||||
<IconButton color="inherit">
|
||||
<GitHubIcon
|
||||
sx={{
|
||||
display: "inline-block",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: "100%", height: "4rem" }} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1 +1 @@
|
||||
export { Layout } from "./layout"
|
||||
export { LandingPageLayout } from "./layout";
|
||||
|
@ -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 (
|
||||
<Box sx={{ width: "100vw", textAlign: "end", px: 2, py: 1 }}>
|
||||
<Link
|
||||
href="https://github.com/nix-community/noogle"
|
||||
target="_blank"
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<Tooltip title="Github">
|
||||
<IconButton color="inherit">
|
||||
<GitHubIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://nixos.org"
|
||||
target="_blank"
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
<Tooltip title="NixOS">
|
||||
<IconButton color="inherit">
|
||||
<PublicIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export function Layout(props: LayoutProps) {
|
||||
export function LandingPageLayout(props: LayoutProps) {
|
||||
const { children } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100vh",
|
||||
overflow: "scroll",
|
||||
bgcolor:
|
||||
theme.palette.mode === "light" ? "rgb(242, 248, 253)" : "#070c16",
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
zIndex: 0,
|
||||
opacity: 0.1,
|
||||
backgroundImage: `url(${nixSnowflake.src})`,
|
||||
backgroundPositionX: "50%",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 1,
|
||||
zIndex: 1,
|
||||
borderBottomRightRadius: 16,
|
||||
borderBottomLeftRadius: 16,
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light"
|
||||
? "primary.main"
|
||||
: "background.paper",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
component="h1"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
color: "#fff",
|
||||
fontSize: "30pt",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: {
|
||||
xs: "none",
|
||||
md: "inline-block",
|
||||
},
|
||||
}}
|
||||
component="span"
|
||||
>
|
||||
<Image
|
||||
src={nixWhite}
|
||||
alt="nix-logo"
|
||||
height={25}
|
||||
style={{
|
||||
marginBottom: "0rem",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ ml: 1 }} component="span">{`noog\u03BBe`}</Box>
|
||||
<Link
|
||||
href="https://github.com/nix-community/noogle"
|
||||
target="_blank"
|
||||
>
|
||||
<Tooltip title="Contribute on Github">
|
||||
<IconButton
|
||||
sx={{ float: "right", top: "0.6rem", right: "1em", p: 0 }}
|
||||
>
|
||||
<GitHubIcon
|
||||
sx={{
|
||||
display: {
|
||||
xs: "none",
|
||||
md: "inline-block",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</header>
|
||||
<Background>
|
||||
<SocialIcons />
|
||||
<main
|
||||
style={{
|
||||
marginTop: "1rem",
|
||||
marginTop: "4.8rem",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
<Container sx={{ pt: 0, px: { xs: 0, md: 2 } }} maxWidth="xl">
|
||||
<Container fixed sx={{ px: 0 }}>
|
||||
{children}
|
||||
</Container>
|
||||
</main>
|
||||
<footer
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
fontSize: "0.8rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
placeItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
>
|
||||
Powered by{" "}
|
||||
<Link sx={{ ml: 1 }} href="https://oceansprint.org/">
|
||||
{" "}
|
||||
OceanSprint
|
||||
</Link>
|
||||
</Link>{" "}
|
||||
© 2023 Noogle. All rights reserved.
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ maxWidth: "100rem", fontSize: "0.7rem" }}
|
||||
>
|
||||
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.
|
||||
</Typography>
|
||||
</footer>
|
||||
</Box>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: "h3",
|
||||
h2: "h4",
|
||||
h3: "h5",
|
||||
h4: "h6",
|
||||
}}
|
||||
rehypePlugins={[[rehypeHighlight, { languages: { nix } }]]}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
<>
|
||||
<HighlightBaseline />
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: "h3",
|
||||
h2: "h4",
|
||||
h3: "h5",
|
||||
h4: "h6",
|
||||
}}
|
||||
rehypePlugins={[[rehypeHighlight, { languages: { nix } }]]}
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,3 +0,0 @@
|
||||
export { PageContextProvider, PageContext, usePageContext} from "./pageContext";
|
||||
|
||||
export type { SetPageStateVariable } from "./pageContext";
|
@ -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<React.SetStateAction<PageState>>;
|
||||
setPageStateVariable: SetPageStateVariable;
|
||||
resetQueries: () => void;
|
||||
};
|
||||
|
||||
const initialState = { ...initialPageState, FOTD: true };
|
||||
export const PageContext = React.createContext<PageContextType>({
|
||||
pageState: initialState,
|
||||
setPageState: () => {},
|
||||
resetQueries: () => {},
|
||||
setPageStateVariable: function a<T>() {
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
interface PageContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
pageProps: PageState;
|
||||
}
|
||||
|
||||
export type SetPageStateVariable = <T>(
|
||||
field: keyof InitialPageState
|
||||
) => (value: React.SetStateAction<T> | T) => void;
|
||||
|
||||
const paramList: Omit<keyof PageState, "data">[] = [
|
||||
"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<PageState>(pageProps);
|
||||
const { term, filter, viewMode } = pageState;
|
||||
|
||||
function setPageStateVariable<T>(field: keyof InitialPageState) {
|
||||
return function (value: React.SetStateAction<T> | 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 (
|
||||
<PageContext.Provider
|
||||
value={{
|
||||
pageState,
|
||||
setPageState,
|
||||
setPageStateVariable,
|
||||
resetQueries,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePageContext = () => React.useContext(PageContext);
|
@ -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 (
|
||||
<Box
|
||||
@ -59,49 +34,17 @@ export const Preview = (props: PreviewProps) => {
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
color={"text.primary"}
|
||||
sx={{ wordWrap: "normal", lineBreak: "anywhere" }}
|
||||
>
|
||||
{`${meta.title}`}
|
||||
</Typography>
|
||||
{closeComponent || (
|
||||
<Tooltip title="close details">
|
||||
<IconButton
|
||||
sx={{
|
||||
mx: { xs: "auto", md: 1 },
|
||||
left: { lg: "calc(50% - 2rem)", xs: "unset" },
|
||||
position: { lg: "absolute", xs: "relative" },
|
||||
}}
|
||||
size="small"
|
||||
onClick={() => handleClose?.()}
|
||||
>
|
||||
<ExpandLessIcon fontSize="large" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
{/* {prefix !== "builtins" && id.includes("lib.") && (
|
||||
<Box sx={{ my: 1 }}>
|
||||
<Typography variant="subtitle1">
|
||||
{`short form: lib.${name}`}
|
||||
<Link href={`/f/${meta.path.join("/")}`}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ wordWrap: "normal", lineBreak: "anywhere" }}
|
||||
>
|
||||
{`${meta.title}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)} */}
|
||||
</Link>
|
||||
</Box>
|
||||
<List sx={{ width: "100%" }} disablePadding>
|
||||
<ListItem sx={{ flexDirection: { xs: "column", sm: "row" }, px: 0 }}>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={"read docs"}>
|
||||
<MuiLink
|
||||
sx={{ m: "auto", color: "primary.light" }}
|
||||
target="_blank"
|
||||
href={content?.source?.position?.file || "#"}
|
||||
>
|
||||
<LocalLibraryIcon sx={{ m: "auto" }} />
|
||||
</MuiLink>
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
@ -110,43 +53,17 @@ export const Preview = (props: PreviewProps) => {
|
||||
width: "100%",
|
||||
px: 0,
|
||||
}}
|
||||
primaryTypographyProps={{
|
||||
color: "text.secondary",
|
||||
fontSize: 14,
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
color: "text.primary",
|
||||
fontSize: "1rem",
|
||||
component: "div",
|
||||
}}
|
||||
// primary={
|
||||
// !id.includes("builtins") ? (
|
||||
// <Tooltip title={"browse source code"}>
|
||||
// <MuiLink
|
||||
// target={"_blank"}
|
||||
// href={`https://github.com/NixOS/nixpkgs/blob/master/${category.replace(
|
||||
// "./",
|
||||
// ""
|
||||
// )}#L${line}`}
|
||||
// >
|
||||
// {"github:NixOS/nixpkgs/" + category.replace("./", "")}
|
||||
// </MuiLink>
|
||||
// </Tooltip>
|
||||
// ) : (
|
||||
// "github:NixOS/nix/" + category.replace("./", "")
|
||||
// )
|
||||
// }
|
||||
secondary={
|
||||
<Container
|
||||
primary={
|
||||
<Box
|
||||
component={"div"}
|
||||
sx={{
|
||||
ml: "0 !important",
|
||||
pl: "0 !important",
|
||||
overflow: "visible",
|
||||
width: "100%",
|
||||
}}
|
||||
maxWidth="md"
|
||||
>
|
||||
{meta?.is_primop && (
|
||||
{meta?.is_primop && meta.primop_meta && (
|
||||
<MarkdownPreview
|
||||
description={getPrimopDescription(meta.primop_meta)}
|
||||
/>
|
||||
@ -154,56 +71,15 @@ export const Preview = (props: PreviewProps) => {
|
||||
{!!content?.content && (
|
||||
<MarkdownPreview description={content.content} />
|
||||
)}
|
||||
</Container>
|
||||
{!content?.content && (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Not documented yet. Contribute now!
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem sx={{ flexDirection: { xs: "column", sm: "row" }, px: 0 }}>
|
||||
<ListItemIcon>
|
||||
<Tooltip title={"browse source code"}>
|
||||
{/* <MuiLink
|
||||
sx={{ m: "auto", color: "primary.light" }}
|
||||
target="_blank"
|
||||
href={`https://github.com/NixOS/nixpkgs/blob/master/${category.replace(
|
||||
"./",
|
||||
""
|
||||
)}`}
|
||||
> */}
|
||||
<InputIcon sx={{ m: "auto" }} />
|
||||
{/* </MuiLink> */}
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis",
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
primaryTypographyProps={{
|
||||
color: "text.secondary",
|
||||
gutterBottom: true,
|
||||
fontSize: 14,
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
// color: fn_type ? "text.primary" : "text.secondary",
|
||||
fontSize: theme.typography.fontSize + 4,
|
||||
}}
|
||||
secondary={
|
||||
"Types cannot be detected yet. Work with us on migrating this feature."
|
||||
// fn_type ? (
|
||||
// <CodeHighlight
|
||||
// code={fn_type}
|
||||
// theme={theme.palette.mode}
|
||||
// lang={"Haskell"}
|
||||
// />
|
||||
// ) : (
|
||||
// "no type provided yet."
|
||||
// )
|
||||
}
|
||||
primary="function signature "
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
@ -37,7 +37,6 @@ export default function SearchPage() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("result", { results, query });
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
|
@ -1,195 +1,125 @@
|
||||
"use client";
|
||||
import { NixType } from "@/models/nix";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Grid,
|
||||
Input,
|
||||
Typography,
|
||||
debounce,
|
||||
} from "@mui/material";
|
||||
import { Autocomplete, Divider, Input } from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { SearchOptions, Suggestion } from "minisearch";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { usePageContext } from "../pageContext";
|
||||
import React, { useState } from "react";
|
||||
import { data } from "@/models/data";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export type Filter = { from: NixType; to: NixType };
|
||||
|
||||
export interface SearchInputProps {
|
||||
handleSearch: (term: string) => 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<string>("");
|
||||
|
||||
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<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
_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 (
|
||||
<>
|
||||
<Paper
|
||||
component="form"
|
||||
elevation={0}
|
||||
<Paper
|
||||
component="form"
|
||||
action="q"
|
||||
elevation={0}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
bgcolor: "inherit",
|
||||
}}
|
||||
onSubmit={(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(term);
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
includeInputInList
|
||||
aria-label={"search-input"}
|
||||
onInputChange={(e, value, reason) => {
|
||||
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<HTMLInputElement>);
|
||||
}}
|
||||
value={term}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
<Input
|
||||
disableUnderline
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
ml: 1,
|
||||
flex: 1,
|
||||
backgroundColor: "paper.main",
|
||||
px: 1,
|
||||
py: 0,
|
||||
},
|
||||
}}
|
||||
value={term}
|
||||
onChange={(e) => handleType(e)}
|
||||
placeholder={placeholder}
|
||||
{...params}
|
||||
{...params.InputProps}
|
||||
endAdornment={undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton aria-label="clear-button" onClick={_handleClear} size="small">
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<Divider flexItem orientation="vertical" sx={{ mx: 1 }} />
|
||||
<IconButton
|
||||
type="submit"
|
||||
size="small"
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: 1,
|
||||
my: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onSubmit={(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(term);
|
||||
}}
|
||||
aria-label="search-button"
|
||||
>
|
||||
<IconButton aria-label="clear-button" onClick={_handleClear}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
|
||||
<Autocomplete
|
||||
// disablePortal
|
||||
// id="combo-box-demo"
|
||||
options={autoCompleteOptions}
|
||||
sx={{ width: "100%" }}
|
||||
onChange={(e, value) => {
|
||||
handleType({
|
||||
target: { value: value || "" },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
}}
|
||||
value={_term}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
// <InputBase {...params} {...params.InputProps} />
|
||||
<Input
|
||||
disableUnderline
|
||||
autoFocus
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
ml: 1,
|
||||
flex: 1,
|
||||
backgroundColor: "paper.main",
|
||||
p: 1,
|
||||
},
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
// inputProps={{ "aria-label": "search-input" }}
|
||||
value={_term}
|
||||
onChange={(e) => handleType(e)}
|
||||
{...params}
|
||||
{...params.InputProps}
|
||||
endAdornment={undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
sx={{
|
||||
p: 1,
|
||||
}}
|
||||
aria-label="search-button"
|
||||
onClick={() => handleSearch(_term)}
|
||||
>
|
||||
<SearchIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
|
||||
<Box>
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={5}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Type search is temporarily unavailable. Due to ongoing RFC-145
|
||||
integration
|
||||
</Typography>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} md={5}>
|
||||
<SelectOption
|
||||
value={filter.from}
|
||||
label="from type"
|
||||
handleChange={(value) => {
|
||||
handleFilter((curr) => ({ ...curr, from: value as NixType }));
|
||||
}}
|
||||
options={nixTypes.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
md={2}
|
||||
sx={{
|
||||
display: {
|
||||
md: "flex",
|
||||
xs: "none",
|
||||
},
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
<ChevronRightIcon />
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5}>
|
||||
<SelectOption
|
||||
value={filter.to}
|
||||
label="to type"
|
||||
handleChange={(value) => {
|
||||
handleFilter((curr) => ({ ...curr, to: value as NixType }));
|
||||
}}
|
||||
options={nixTypes.map((v) => ({ value: v, label: v }))}
|
||||
/>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
<SearchIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
// app/ThemeRegistry.tsx
|
||||
"use client";
|
||||
import { darkThemeOptions, lightThemeOptions } from "@/styles/theme";
|
||||
import createCache from "@emotion/cache";
|
||||
|
77
website/src/excerpt.ts
Normal file
77
website/src/excerpt.ts
Normal file
@ -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 };
|
0
website/src/fonts/index
Normal file
0
website/src/fonts/index
Normal file
@ -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)
|
||||
);
|
||||
|
8
website/src/models/primop.ts
Normal file
8
website/src/models/primop.ts
Normal file
@ -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`;
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
18
website/src/styles/theme/common.ts
Normal file
18
website/src/styles/theme/common.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ThemeOptions } from "@mui/material/styles";
|
||||
|
||||
const commonOptions: Partial<ThemeOptions> = {
|
||||
typography: {
|
||||
fontFamily: "inherit",
|
||||
h1: {
|
||||
fontSize: "2.9rem",
|
||||
},
|
||||
h2: {
|
||||
fontSize: "2.6rem",
|
||||
},
|
||||
h3: {
|
||||
fontSize: "2.3rem",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { commonOptions };
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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<Heading[]> => {
|
||||
export const extractHeadings = async (content: string): Promise<Heading[]> => {
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkHeadingId)
|
||||
|
Loading…
Reference in New Issue
Block a user