redesign with subpages

This commit is contained in:
Johannes Kirschbauer 2023-12-22 20:34:22 +01:00 committed by Johannes Kirschbauer
parent 26e9d0e7a3
commit f637df2d2d
74 changed files with 2622 additions and 1376 deletions

10
codemod/.envrc Normal file
View 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
View File

@ -0,0 +1,2 @@
/target
result*

221
codemod/Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
}

View File

@ -0,0 +1,17 @@
{
/*
Header
Example:
assertMsg false "nope"
Type:
assertMsg :: Bool -> String -> Bool
*/
stuff =
# a arg
a:
# b arg
b: a;
}

View 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
View 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

View File

@ -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"
}

View File

@ -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
];

View File

@ -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; };

View File

@ -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 =

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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
}
})

View File

@ -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

View 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"
]
}
]

View 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"]
}
]

View File

@ -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,

View 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"
}
]

View 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
View File

@ -10,6 +10,10 @@ coverage
src/models/data/*
!src/models/data/index.ts
src/fonts/*
!src/fonts/index
# nix
.direnv/

View File

@ -19,6 +19,7 @@
devShells.ui = pkgs.callPackage ./shell.nix {
inherit pkgs hooks;
inherit (base) fmod pkg;
inherit (self'.packages) data-json pasta-meta;
};
};
}

View File

@ -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 = {

View File

@ -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",

View File

@ -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": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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)

View 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>
</>
);
}

View 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>
</>
);
}

View File

@ -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>
);

View File

@ -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>
);
}

View 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}
</>
);
}

View 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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View 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>
);
};

View 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>
</>
);
};

View 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 <></>;
};

View File

@ -1 +0,0 @@
export {NixFunctions as default} from "./nixFunctions";

View File

@ -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>
);
}

View 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>
);
}

View 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>
);
};

View File

@ -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>
)}
</>
);
}

View File

@ -1 +0,0 @@
export { AppState } from "./appState";

View 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>
);
};

View File

@ -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>
);
}

View File

@ -1,2 +0,0 @@
export { BasicList } from "./basicList";
export type { BasicListProps, BasicListItem } from "./basicList";

View File

@ -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>
</>
);
};

View File

@ -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>
);
}

View File

@ -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>
);
};

View 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>
);
};

View 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" }} />
</>
);
};

View File

@ -1 +1 @@
export { Layout } from "./layout"
export { LandingPageLayout } from "./layout";

View File

@ -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>
);
}

View File

@ -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>
</>
);
};

View File

@ -1,3 +0,0 @@
export { PageContextProvider, PageContext, usePageContext} from "./pageContext";
export type { SetPageStateVariable } from "./pageContext";

View File

@ -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);

View File

@ -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>
);

View File

@ -37,7 +37,6 @@ export default function SearchPage() {
}
}
console.log("result", { results, query });
return (
<div>
<input

View File

@ -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>
);
}

View File

@ -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
View 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
View File

View 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)
);

View 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`;
};

View File

@ -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;
}
}

View 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 };

View File

@ -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",
},
},
};

View File

@ -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",
},
},
};

View File

@ -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)