My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: kasten implementation

+306 -81
+148 -3
Cargo.lock
··· 24 24 source = "registry+https://github.com/rust-lang/crates.io-index" 25 25 checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" 26 26 dependencies = [ 27 + "enumn", 28 + "serde", 27 29 "uuid", 28 30 ] 29 31 ··· 147 149 "const-random", 148 150 "getrandom 0.3.4", 149 151 "once_cell", 152 + "serde", 150 153 "version_check", 151 154 "zerocopy", 152 155 ] ··· 1508 1511 ] 1509 1512 1510 1513 [[package]] 1514 + name = "crossbeam-deque" 1515 + version = "0.8.6" 1516 + source = "registry+https://github.com/rust-lang/crates.io-index" 1517 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 1518 + dependencies = [ 1519 + "crossbeam-epoch", 1520 + "crossbeam-utils", 1521 + ] 1522 + 1523 + [[package]] 1524 + name = "crossbeam-epoch" 1525 + version = "0.9.18" 1526 + source = "registry+https://github.com/rust-lang/crates.io-index" 1527 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 1528 + dependencies = [ 1529 + "crossbeam-utils", 1530 + ] 1531 + 1532 + [[package]] 1511 1533 name = "crossbeam-queue" 1512 1534 version = "0.3.12" 1513 1535 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1889 1911 dependencies = [ 1890 1912 "bytemuck", 1891 1913 "emath", 1914 + "serde", 1892 1915 ] 1893 1916 1894 1917 [[package]] ··· 1941 1964 "log", 1942 1965 "nohash-hasher", 1943 1966 "profiling", 1967 + "ron", 1968 + "serde", 1944 1969 "smallvec", 1945 1970 "unicode-segmentation", 1946 1971 ] ··· 2002 2027 "wasm-bindgen", 2003 2028 "web-sys", 2004 2029 "winit", 2030 + ] 2031 + 2032 + [[package]] 2033 + name = "egui_graphs" 2034 + version = "0.30.0" 2035 + source = "git+https://github.com/suri-codes/egui_graphs?rev=3327417007520300782574e3d6bc377bbafa8b5a#3327417007520300782574e3d6bc377bbafa8b5a" 2036 + dependencies = [ 2037 + "egui", 2038 + "getrandom 0.2.17", 2039 + "petgraph", 2040 + "rand 0.9.2", 2041 + "serde", 2042 + "web-time", 2005 2043 ] 2006 2044 2007 2045 [[package]] ··· 2020 2058 checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" 2021 2059 dependencies = [ 2022 2060 "bytemuck", 2061 + "serde", 2023 2062 ] 2024 2063 2025 2064 [[package]] ··· 2065 2104 ] 2066 2105 2067 2106 [[package]] 2107 + name = "enumn" 2108 + version = "0.1.14" 2109 + source = "registry+https://github.com/rust-lang/crates.io-index" 2110 + checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" 2111 + dependencies = [ 2112 + "proc-macro2", 2113 + "quote", 2114 + "syn 2.0.117", 2115 + ] 2116 + 2117 + [[package]] 2068 2118 name = "epaint" 2069 2119 version = "0.34.1" 2070 2120 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2081 2131 "parking_lot", 2082 2132 "profiling", 2083 2133 "self_cell", 2134 + "serde", 2084 2135 "skrifa", 2085 2136 "smallvec", 2086 2137 "vello_cpu", ··· 2248 2299 "directories", 2249 2300 "dto", 2250 2301 "eframe", 2302 + "egui_graphs", 2251 2303 "futures", 2252 2304 "human-panic", 2253 2305 "kdl", 2306 + "pulldown-cmark", 2307 + "rand 0.10.0", 2254 2308 "ratatui", 2309 + "rayon", 2255 2310 "serde", 2256 2311 "signal-hook 0.4.3", 2257 2312 "strum 0.28.0", ··· 2304 2359 checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 2305 2360 2306 2361 [[package]] 2362 + name = "fixedbitset" 2363 + version = "0.5.7" 2364 + source = "registry+https://github.com/rust-lang/crates.io-index" 2365 + checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 2366 + 2367 + [[package]] 2307 2368 name = "flate2" 2308 2369 version = "1.1.9" 2309 2370 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2349 2410 checksum = "73829a7b5c91198af28a99159b7ae4afbb252fb906159ff7f189f3a2ceaa3df2" 2350 2411 dependencies = [ 2351 2412 "bytemuck", 2413 + "serde", 2352 2414 ] 2353 2415 2354 2416 [[package]] ··· 2526 2588 ] 2527 2589 2528 2590 [[package]] 2591 + name = "getopts" 2592 + version = "0.2.24" 2593 + source = "registry+https://github.com/rust-lang/crates.io-index" 2594 + checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" 2595 + dependencies = [ 2596 + "unicode-width 0.2.2", 2597 + ] 2598 + 2599 + [[package]] 2529 2600 name = "getrandom" 2530 2601 version = "0.2.17" 2531 2602 source = "registry+https://github.com/rust-lang/crates.io-index" 2532 2603 checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 2533 2604 dependencies = [ 2534 2605 "cfg-if", 2606 + "js-sys", 2535 2607 "libc", 2536 2608 "wasi", 2609 + "wasm-bindgen", 2537 2610 ] 2538 2611 2539 2612 [[package]] ··· 3425 3498 "log", 3426 3499 "presser", 3427 3500 "thiserror 2.0.18", 3428 - "windows 0.61.3", 3501 + "windows 0.62.2", 3429 3502 ] 3430 3503 3431 3504 [[package]] ··· 3629 3702 "js-sys", 3630 3703 "log", 3631 3704 "wasm-bindgen", 3632 - "windows-core 0.61.2", 3705 + "windows-core 0.62.2", 3633 3706 ] 3634 3707 3635 3708 [[package]] ··· 5128 5201 ] 5129 5202 5130 5203 [[package]] 5204 + name = "petgraph" 5205 + version = "0.8.3" 5206 + source = "registry+https://github.com/rust-lang/crates.io-index" 5207 + checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" 5208 + dependencies = [ 5209 + "fixedbitset 0.5.7", 5210 + "hashbrown 0.15.5", 5211 + "indexmap", 5212 + "serde", 5213 + "serde_derive", 5214 + ] 5215 + 5216 + [[package]] 5131 5217 name = "pgvector" 5132 5218 version = "0.4.1" 5133 5219 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5494 5580 ] 5495 5581 5496 5582 [[package]] 5583 + name = "pulldown-cmark" 5584 + version = "0.13.3" 5585 + source = "registry+https://github.com/rust-lang/crates.io-index" 5586 + checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" 5587 + dependencies = [ 5588 + "bitflags 2.11.0", 5589 + "getopts", 5590 + "memchr", 5591 + "pulldown-cmark-escape", 5592 + "unicase", 5593 + ] 5594 + 5595 + [[package]] 5596 + name = "pulldown-cmark-escape" 5597 + version = "0.11.0" 5598 + source = "registry+https://github.com/rust-lang/crates.io-index" 5599 + checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 5600 + 5601 + [[package]] 5497 5602 name = "pxfm" 5498 5603 version = "0.1.28" 5499 5604 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5737 5842 ] 5738 5843 5739 5844 [[package]] 5845 + name = "rayon" 5846 + version = "1.11.0" 5847 + source = "registry+https://github.com/rust-lang/crates.io-index" 5848 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 5849 + dependencies = [ 5850 + "either", 5851 + "rayon-core", 5852 + ] 5853 + 5854 + [[package]] 5855 + name = "rayon-core" 5856 + version = "1.13.0" 5857 + source = "registry+https://github.com/rust-lang/crates.io-index" 5858 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 5859 + dependencies = [ 5860 + "crossbeam-deque", 5861 + "crossbeam-utils", 5862 + ] 5863 + 5864 + [[package]] 5740 5865 name = "read-fonts" 5741 5866 version = "0.37.0" 5742 5867 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5864 5989 "proc-macro2", 5865 5990 "quote", 5866 5991 "syn 1.0.109", 5992 + ] 5993 + 5994 + [[package]] 5995 + name = "ron" 5996 + version = "0.12.0" 5997 + source = "registry+https://github.com/rust-lang/crates.io-index" 5998 + checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" 5999 + dependencies = [ 6000 + "bitflags 2.11.0", 6001 + "once_cell", 6002 + "serde", 6003 + "serde_derive", 6004 + "typeid", 6005 + "unicode-ident", 5867 6006 ] 5868 6007 5869 6008 [[package]] ··· 6984 7123 "fancy-regex", 6985 7124 "filedescriptor", 6986 7125 "finl_unicode", 6987 - "fixedbitset", 7126 + "fixedbitset 0.4.2", 6988 7127 "hex", 6989 7128 "lazy_static", 6990 7129 "libc", ··· 7373 7512 dependencies = [ 7374 7513 "rustc-hash 2.1.1", 7375 7514 ] 7515 + 7516 + [[package]] 7517 + name = "typeid" 7518 + version = "1.0.3" 7519 + source = "registry+https://github.com/rust-lang/crates.io-index" 7520 + checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" 7376 7521 7377 7522 [[package]] 7378 7523 name = "typenum"
+8
Cargo.toml
··· 75 75 dto = {path="./crates/dto"} 76 76 eframe = "0.34.1" 77 77 async-trait = "0.1.89" 78 + egui_graphs = "0.30.0" 79 + rayon = "1.11.0" 80 + rand = "0.10.0" 81 + pulldown-cmark = { version = "0.13.3", features = ["simd"] } 78 82 79 83 [build-dependencies] 80 84 anyhow = "1.0.102" 81 85 vergen-gix = {version = "9.1.0", features = ["build", "cargo"]} 86 + 87 + # patch egui_graphs to have egui 0.34.1 88 + [patch.crates-io] 89 + egui_graphs = { git = "https://github.com/suri-codes/egui_graphs", rev = "3327417007520300782574e3d6bc377bbafa8b5a" } 82 90 83 91 [profile.release] 84 92 codegen-units = 1
+3 -1
crates/dto/migration/src/types/nano_id.rs
··· 4 4 use sea_orm::DeriveValueType; 5 5 use serde::{Deserialize, Serialize}; 6 6 7 - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, DeriveValueType)] 7 + #[derive( 8 + Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, DeriveValueType, 9 + )] 8 10 #[sea_orm(value_type = "String")] 9 11 pub struct NanoId(pub(crate) String); 10 12
+1
flake.nix
··· 95 95 cargo-edit 96 96 cargo-watch 97 97 cargo-nextest 98 + 98 99 rust-analyzer 99 100 100 101 sea-orm-cli
+11
src/types/frontmatter.rs
··· 2 2 3 3 // use chrono::format::StrftimeItems; 4 4 use color_eyre::eyre::{Result, eyre}; 5 + use egui_graphs::Node; 5 6 use serde::{Deserialize, Serialize}; 6 7 use tokio::fs; 7 8 8 9 use dto::DateTime; 10 + 11 + use crate::types::{Link, Zettel}; 9 12 10 13 const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 11 14 ··· 29 32 created_at, 30 33 tag_strings, 31 34 } 35 + } 36 + 37 + /// Apply the features of `FrontMatter` onto a 38 + /// `Node` 39 + pub fn apply_node_transform(&self, node: &mut Node<Zettel, Link>) { 40 + node.set_label(self.title.clone()); 41 + let disp = node.display_mut(); 42 + disp.radius = 100.0; 32 43 } 33 44 34 45 /// Reads in file and returns the front matter as well as the content after it.
+86 -25
src/types/kasten.rs
··· 1 - use std::path::PathBuf; 2 - 1 + use crate::types::{Link, Zettel, ZettelId}; 3 2 use color_eyre::eyre::Result; 3 + use eframe::emath; 4 + use egui_graphs::{ 5 + Graph, 6 + petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, 7 + }; 8 + use rayon::iter::{ParallelBridge as _, ParallelIterator as _}; 9 + use std::{cmp::max, collections::HashMap}; 4 10 5 11 use crate::types::Workspace; 6 12 7 - /// The `Kasten` that stores the `Link`s between `Zettel`s 8 13 #[derive(Debug, Clone)] 9 14 #[expect(dead_code)] 10 15 pub struct Kasten { 11 16 /// Private field so it can only be instantiated from a `Path` 12 17 _private: (), 13 - // / Connection to the sqlite database inside the `Workspace` 18 + 19 + /// The workspace this `Kasten` is in 14 20 pub ws: Workspace, 21 + 22 + /// the graph of `Zettel`s and the `Links` between them 23 + pub graph: ZkGraph, 24 + 25 + /// simple conversions 26 + zid_to_gid: HashMap<ZettelId, NodeIndex>, 27 + 28 + pub most_recently_edited: Option<NodeIndex>, 15 29 } 16 30 31 + pub type ZkGraph = Graph<Zettel, Link, Directed>; 32 + 33 + /// Minimum number of nodes in our graph. 34 + const GRAPH_MIN_NODES: usize = 128; 35 + /// Arbitrarily chosen minimum number of edges 36 + const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3; 37 + 17 38 impl Kasten { 18 - /// Given a path, try to construct a `Kasten`. 19 - pub async fn index(root: impl Into<PathBuf>) -> Result<Self> { 20 - let ws = Workspace::instansiate(root).await?; 39 + /// Indexes the `Workspace` and constructs a `Kasten` 40 + #[expect(dead_code)] 41 + pub async fn index(ws: Workspace) -> Result<Self> { 42 + let paths = std::fs::read_dir(&ws.root)? 43 + .par_bridge() 44 + .flatten() 45 + .filter(|entry| { 46 + entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) 47 + && entry 48 + .path() 49 + .extension() 50 + .and_then(|ext| ext.to_str()) 51 + .is_some_and(|ext| ext == "md") 52 + }) 53 + .map(|entry| entry.path()) 54 + .collect::<Vec<_>>(); 55 + 56 + let zettel_tasks = paths 57 + .into_iter() 58 + .map(|path| { 59 + let ws = ws.clone(); 60 + tokio::spawn(async move { Zettel::from_path(path, &ws).await }) 61 + }) 62 + .collect::<Vec<_>>(); 63 + 64 + // await all of them 65 + let zettels = futures::future::join_all(zettel_tasks) 66 + .await 67 + .into_iter() 68 + .filter_map(|result| result.ok()?.ok()) 69 + .collect::<Vec<Zettel>>(); 21 70 22 - Ok(Self { _private: (), ws }) 23 - } 24 - } 71 + // capacity! 72 + let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity( 73 + max(zettels.len() * 2, GRAPH_MIN_EDGES), 74 + max(zettels.len() * 3, GRAPH_MIN_EDGES), 75 + )); 25 76 26 - #[cfg(test)] 27 - mod tests { 28 - use std::{ 29 - fs::{File, create_dir_all}, 30 - path::PathBuf, 31 - }; 77 + let mut zid_to_gid = HashMap::new(); 78 + for zettel in &zettels { 79 + let fm = zettel.front_matter(&ws).await?; 80 + let id = graph.add_node_custom(zettel.clone(), |node| { 81 + fm.apply_node_transform(node); 82 + let x = rand::random_range(0.0..=100.0); 83 + let y = rand::random_range(0.0..=100.0); 84 + node.set_location(emath::Pos2 { x, y }); 85 + }); 86 + zid_to_gid.insert(zettel.id.clone(), id); 87 + } 32 88 33 - use crate::types::Kasten; 89 + for zettel in &zettels { 90 + let src = zid_to_gid.get(&zettel.id).expect("must exist"); 91 + for link in &zettel.links(&ws).await? { 92 + let dst = zid_to_gid.get(&link.dest).expect("must exist"); 93 + graph.add_edge(*src, *dst, link.clone()); 94 + } 95 + } 34 96 35 - #[tokio::test] 36 - async fn test_instantiation() { 37 - let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db"); 38 - create_dir_all(path.parent().unwrap()).unwrap(); 39 - let _ = File::create(&path).unwrap(); 40 - let _k = Kasten::index(dbg!(&path.parent().unwrap().parent().unwrap())) 41 - .await 42 - .unwrap(); 97 + Ok(Self { 98 + _private: (), 99 + ws, 100 + graph, 101 + zid_to_gid, 102 + most_recently_edited: None, 103 + }) 43 104 } 44 105 }
+1
src/types/mod.rs
··· 25 25 pub use link::Link; 26 26 27 27 mod kasten; 28 + #[expect(unused_imports)] 28 29 pub use kasten::Kasten; 29 30 30 31 mod frontmatter;
+1 -1
src/types/workspace.rs
··· 85 85 let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db"); 86 86 create_dir_all(path.parent().unwrap()).unwrap(); 87 87 let _ = File::create(&path).unwrap(); 88 - let _k = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap())) 88 + let _ws = Workspace::instansiate(&path.parent().unwrap().parent().unwrap()) 89 89 .await 90 90 .unwrap(); 91 91 }
+47 -51
src/types/zettel.rs
··· 3 3 QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx, 4 4 ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity, 5 5 }; 6 + use pulldown_cmark::{Event, Parser, Tag as MkTag}; 6 7 use serde::{Deserialize, Serialize}; 7 8 use std::{ 8 9 fmt::Display, 9 10 path::{Path, PathBuf}, 10 11 }; 11 - use tracing::info; 12 + use tracing::{error, info}; 12 13 13 14 use color_eyre::eyre::{Error, Result, eyre}; 14 15 use dto::NanoId; 15 16 use tokio::{fs::File, io::AsyncWriteExt}; 16 17 17 - use crate::types::{FrontMatter, Tag, Workspace}; 18 + use crate::types::{FrontMatter, Link, Tag, Workspace}; 18 19 19 20 /// A `Zettel` is a note about a single idea. 20 21 /// It can have many `Tag`s, just meaning it can fall under many ··· 35 36 /// A `ZettelId` is essentially a `NanoId`, 36 37 /// with some `Zettel` specific helpers written 37 38 /// onto it 38 - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] 39 + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 39 40 pub struct ZettelId(NanoId); 40 41 41 42 impl Zettel { ··· 80 81 81 82 /// Returns the most up-to-date `FrontMatter` for this 82 83 /// `Zettel` 83 - #[expect(dead_code)] 84 84 pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> { 85 85 let path = self.absolute_path(ws); 86 86 let (fm, _) = FrontMatter::extract_from_file(path).await?; ··· 89 89 90 90 /// Returns the content of this `Zettel`, which is everything 91 91 /// but the `FrontMatter` 92 - #[expect(dead_code)] 93 92 pub async fn content(&self, ws: &Workspace) -> Result<String> { 94 93 let path = self.absolute_path(ws); 95 94 let (_, content) = FrontMatter::extract_from_file(path).await?; ··· 107 106 } 108 107 109 108 /// uses the id and root to parse out of the root directory 109 + #[expect(dead_code)] 110 110 pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> { 111 111 let mut path = ws.root.clone(); 112 112 path.push(id.0.to_string()); ··· 125 125 zettel_tag_strings.sort(); 126 126 127 127 // get the zettel from the db 128 - let db_zettel: ZettelModelEx = if let Some(z) = ZettelEntity::load() 128 + let db_zettel: ZettelModelEx = if let Some(existing_zettel) = ZettelEntity::load() 129 129 .with(TagEntity) 130 130 .filter_by_nano_id(id.clone()) 131 131 .one(&ws.db) 132 132 .await? 133 133 { 134 - z 134 + existing_zettel 135 135 } else { 136 136 // if zettel is missing from db, we just add it here 137 137 info!("adding zettel to db"); ··· 159 159 } else { 160 160 // the db says the file has tag `x`, but that tag is missing from the 161 161 // front matter, we can assume its gone, lets delete that link 162 - let x = ZettelTagEntity::find() 162 + let to_remove = ZettelTagEntity::find() 163 163 .filter(ZettelTagColumns::ZettelNanoId.eq(id.0.clone())) 164 164 .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id)) 165 165 .one(&ws.db) 166 166 .await? 167 167 .expect("this link must exist"); 168 168 169 - x.into_active_model().delete(&ws.db).await?; 169 + to_remove.into_active_model().delete(&ws.db).await?; 170 170 } 171 171 } 172 172 ··· 209 209 .into()) 210 210 } 211 211 212 - // pub fn apply_node_transform(&self, node: &mut Node<Zettel, Link>) { 213 - // node.set_label(self.front_matter.title.to_owned()); 214 - // let disp = node.display_mut(); 215 - // disp.radius = 100.0; 216 - // } 212 + /// The `Link`s that are going out of this `Zettel` 213 + pub async fn links(&self, ws: &Workspace) -> Result<Vec<Link>> { 214 + let content = self.content(ws).await?; 215 + let parsed = Parser::new(&content); 217 216 218 - // fn links_from_content(src_id: &ZettelId, content: &str, ws: &Workspace) -> ZkResult<Vec<Link>> { 219 - // let parsed = Parser::new(content); 220 - 221 - // let mut links = vec![]; 217 + let mut links = vec![]; 222 218 223 - // for event in parsed { 224 - // if let Event::Start(MkTag::Link { dest_url, .. }) = event { 225 - // info!("Found dest_url: {dest_url:#?}"); 219 + for event in parsed { 220 + if let Event::Start(MkTag::Link { dest_url, .. }) = event { 221 + info!("Found dest_url: {dest_url:#?}"); 226 222 227 - // let dest_path = { 228 - // // remove leading "./" 229 - // let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url); 223 + let dest_path = { 224 + // remove leading "./" 225 + let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url); 230 226 231 - // // remove "#" and everything after it 232 - // let without_anchor = without_prefix.split('#').next().unwrap(); 227 + // remove "#" and everything after it 228 + let without_anchor = without_prefix.split('#').next().unwrap(); 233 229 234 - // // add .md if not present 235 - // let normalized = if without_anchor.ends_with(".md") { 236 - // without_anchor.to_string() 237 - // } else { 238 - // format!("{}.md", without_anchor) 239 - // }; 230 + // add .md if not present 231 + let normalized = if std::path::Path::new(without_anchor) 232 + .extension() 233 + .is_some_and(|ext| ext.eq_ignore_ascii_case("md")) 234 + { 235 + without_anchor.to_string() 236 + } else { 237 + format!("{without_anchor}.md") 238 + }; 240 239 241 - // let mut tmp_root = ws.root.clone(); 242 - // tmp_root.push(normalized); 243 - // tmp_root 244 - // }; 245 - // // simplest way to validate that the path exists 246 - // let canon_url = match dest_path.canonicalize() { 247 - // Ok(canon_url) => canon_url, 248 - // Err(_) => { 249 - // error!("Link not found!: {dest_path:?}"); 250 - // continue; 251 - // } 252 - // }; 240 + let mut tmp_root = ws.root.clone(); 241 + tmp_root.push(normalized); 242 + tmp_root 243 + }; 244 + // simplest way to validate that the path exists 245 + let Ok(canon_url) = dest_path.canonicalize() else { 246 + error!("Link not found!: {dest_path:?}"); 247 + continue; 248 + }; 253 249 254 - // let dst_id = ZettelId::try_from(canon_url)?; 250 + let dst_id = ZettelId::try_from(canon_url)?; 255 251 256 - // let link = Link::new(src_id, dst_id); 252 + let link = Link::new(self.id.clone(), dst_id); 257 253 258 - // links.push(link) 259 - // } 260 - // } 254 + links.push(link); 255 + } 256 + } 261 257 262 - // Ok(links) 263 - // } 258 + Ok(links) 259 + } 264 260 } 265 261 266 262 impl From<ZettelModelEx> for Zettel {