Skip to content

Commit 1ca9e89

Browse files
feat: add natural sorting for basename and path fields (#662)
* feat: add natural sorting for basename and path fields * Introduced `PathNatural` and `BasenameNatural` sort fields. * Implemented natural comparison logic for sorting edges based on path and basename. * Updated `EdgeSorter` to handle new sorting criteria. * chore: update version to 4.6.1 in manifest and package files * Increment version number from 4.6.0 to 4.6.1 in manifest-beta.json, manifest.json, and package.json. * Add new version entry for 4.6.1 in versions.json.
1 parent 567e9d3 commit 1ca9e89

8 files changed

Lines changed: 95 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [standa
44

55
## 4.X
66

7+
### [4.6.1](https://github.com/SkepticMystic/breadcrumbs/compare/4.6.0...4.6.1) (2026-04-29)
8+
79
### [4.6.0](https://github.com/SkepticMystic/breadcrumbs/compare/4.5.0...4.6.0) (2026-04-28)
810

911
### [4.5.0](https://github.com/SkepticMystic/breadcrumbs/compare/4.4.4...4.5.0) (2026-04-21)

Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@ build:
3434
bun run build || exit 1
3535
npm i
3636

37-
beta:
38-
npm run version:beta
37+
version_beta:
38+
bun run version-bump-beta.mjs
39+
git add manifest-beta.json versions.json package.json
40+
41+
version_prod:
42+
bun run version-bump.mjs
43+
git add manifest.json versions.json package.json

manifest-beta.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.6.0",
4+
"version": "4.6.1",
55
"minAppVersion": "1.11.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",
88
"authorUrl": "https://github.com/SkepticMystic/breadcrumbs",
99
"fundingUrl": "https://github.com/SkepticMystic/breadcrumbs#donations",
1010
"helpUrl": "https://publish.obsidian.md/breadcrumbs-docs",
1111
"isDesktopOnly": false
12-
}
12+
}

manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.6.0",
4+
"version": "4.6.1",
55
"minAppVersion": "1.11.0",
66
"description": "Add structured hierarchies to your notes",
77
"author": "SkepticMystic",
88
"authorUrl": "https://github.com/SkepticMystic/breadcrumbs",
99
"fundingUrl": "https://github.com/SkepticMystic/breadcrumbs#donations",
1010
"helpUrl": "https://publish.obsidian.md/breadcrumbs-docs",
1111
"isDesktopOnly": false
12-
}
12+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "breadcrumbs",
3-
"version": "4.6.0",
3+
"version": "4.6.1",
44
"description": "Add typed-links to your Obsidian notes",
55
"main": "main.js",
66
"scripts": {

src/const/graph.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const SIMPLE_EDGE_SORT_FIELDS = [
1818
// Hidden because I don't think anyone really cares about that order
1919
// "graph",
2020
"basename",
21+
"basename_natural",
2122
"path",
23+
"path_natural",
2224
"field",
2325
// Whether the edge is explicit or not
2426
// Uses source and implied_kind as tie-breakers for explicit == true and false, respectively

versions.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,5 +253,6 @@
253253
"4.4.4": "1.0.0",
254254
"4.5.0": "1.0.0",
255255
"4.6.0-beta.0": "1.0.0",
256-
"4.6.0": "1.11.0"
257-
}
256+
"4.6.0": "1.11.0",
257+
"4.6.1": "1.11.0"
258+
}

wasm/src/edge_sorting.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use crate::{
1313
#[derive(Clone, Debug)]
1414
pub enum SortField {
1515
Path,
16+
PathNatural,
1617
Basename,
18+
BasenameNatural,
1719
EdgeType,
1820
Implied,
1921
Neighbour(String),
@@ -25,7 +27,9 @@ impl FromStr for SortField {
2527
fn from_str(s: &str) -> Result<Self> {
2628
match s {
2729
"path" => Ok(SortField::Path),
30+
"path_natural" => Ok(SortField::PathNatural),
2831
"basename" => Ok(SortField::Basename),
32+
"basename_natural" => Ok(SortField::BasenameNatural),
2933
"field" => Ok(SortField::EdgeType),
3034
"explicit" => Ok(SortField::Implied),
3135
s if s.starts_with("neighbour-field:") => Ok(SortField::Neighbour(
@@ -122,7 +126,9 @@ impl EdgeSorter {
122126
fn get_edge_comparer<'a>(&self, graph: &'a NoteGraph) -> Comparer<'a> {
123127
match self.field.clone() {
124128
SortField::Path => PathComparer.into(),
129+
SortField::PathNatural => PathNaturalComparer.into(),
125130
SortField::Basename => BasenameComparer.into(),
131+
SortField::BasenameNatural => BasenameNaturalComparer.into(),
126132
SortField::EdgeType => EdgeTypeComparer.into(),
127133
SortField::Implied => ImpliedComparer.into(),
128134
SortField::Neighbour(neighbour_field) => {
@@ -165,12 +171,56 @@ pub trait EdgeComparer {
165171
#[enum_dispatch(EdgeComparer)]
166172
pub enum Comparer<'a> {
167173
PathComparer,
174+
PathNaturalComparer,
168175
BasenameComparer,
176+
BasenameNaturalComparer,
169177
EdgeTypeComparer,
170178
ImpliedComparer,
171179
NeighbourOrdering(NeighbourComparer<'a>),
172180
}
173181

182+
/// Compare two strings using natural sort order: numeric segments are compared
183+
/// numerically so that "note 2" < "note 10" rather than "note 10" < "note 2".
184+
fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
185+
let mut a_chars = a.chars().peekable();
186+
let mut b_chars = b.chars().peekable();
187+
188+
loop {
189+
let a_peek = a_chars.peek().copied();
190+
let b_peek = b_chars.peek().copied();
191+
192+
match (a_peek, b_peek) {
193+
(None, None) => return std::cmp::Ordering::Equal,
194+
(None, _) => return std::cmp::Ordering::Less,
195+
(_, None) => return std::cmp::Ordering::Greater,
196+
(Some(a_ch), Some(b_ch)) if a_ch.is_ascii_digit() && b_ch.is_ascii_digit() => {
197+
let mut a_num = 0u64;
198+
while a_chars.peek().map_or(false, |c| c.is_ascii_digit()) {
199+
a_num = a_num * 10 + (a_chars.next().unwrap() as u64 - '0' as u64);
200+
}
201+
let mut b_num = 0u64;
202+
while b_chars.peek().map_or(false, |c| c.is_ascii_digit()) {
203+
b_num = b_num * 10 + (b_chars.next().unwrap() as u64 - '0' as u64);
204+
}
205+
match a_num.cmp(&b_num) {
206+
std::cmp::Ordering::Equal => continue,
207+
other => return other,
208+
}
209+
}
210+
(Some(a_ch), Some(b_ch)) => {
211+
match a_ch.cmp(&b_ch) {
212+
std::cmp::Ordering::Equal => {
213+
a_chars.next();
214+
b_chars.next();
215+
continue;
216+
}
217+
other => return other,
218+
}
219+
}
220+
}
221+
}
222+
}
223+
174224
#[derive(Default)]
175225
pub struct PathComparer;
176226

@@ -196,6 +246,32 @@ impl EdgeComparer for BasenameComparer {
196246
}
197247
}
198248

249+
#[derive(Default)]
250+
pub struct PathNaturalComparer;
251+
252+
impl EdgeComparer for PathNaturalComparer {
253+
fn compare(&self, graph: &NoteGraph, a: &EdgeStruct, b: &EdgeStruct) -> std::cmp::Ordering {
254+
natural_cmp(
255+
a.target_path_ref(graph).unwrap(),
256+
b.target_path_ref(graph).unwrap(),
257+
)
258+
}
259+
}
260+
261+
#[derive(Default)]
262+
pub struct BasenameNaturalComparer;
263+
264+
impl EdgeComparer for BasenameNaturalComparer {
265+
fn compare(&self, graph: &NoteGraph, a: &EdgeStruct, b: &EdgeStruct) -> std::cmp::Ordering {
266+
let a_target = a.target_path_ref(graph).unwrap();
267+
let b_target = b.target_path_ref(graph).unwrap();
268+
let a_basename = a_target.split('/').next_back().unwrap();
269+
let b_basename = b_target.split('/').next_back().unwrap();
270+
271+
natural_cmp(a_basename, b_basename)
272+
}
273+
}
274+
199275
#[derive(Default)]
200276
pub struct EdgeTypeComparer;
201277

0 commit comments

Comments
 (0)