Files
brahman/crates/modules/badu/badu-core/src/links.rs
T
sergio d0a175a90a feat(badu): toma de notas — núcleo + gravedad semántica
badu-core: modelo Note + NoteStore (etiquetas, búsqueda) + grafo de
wiki-links [[...]] derivado del cuerpo (forward/backlinks, huérfanas,
enlaces colgantes; resolución case-insensitive).

badu-gravity: SemanticField sobre vectores semánticos — afinidad
coseno, vecinos más cercanos, clústeres por umbral (union-find) y
layout 2D dirigido por fuerzas (notas afines se atraen, todas se
repelen; determinista, sin RNG).

29 tests. Cero red, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:42:28 +00:00

94 lines
2.7 KiB
Rust

//! Parser de wiki-links — los destinos `[[...]]` dentro de una nota.
//!
//! Un solo formato: dobles corchetes con el título adentro. El texto se
//! recorta; los enlaces vacíos se descartan; el orden de aparición se
//! conserva y se deduplica (un mismo destino enlazado dos veces cuenta
//! una sola vez como arista).
/// Extrae los destinos `[[...]]` de `text`, recortados y deduplicados,
/// en orden de aparición.
pub fn parse_links(text: &str) -> Vec<String> {
let bytes = text.as_bytes();
let mut out: Vec<String> = Vec::new();
let mut i = 0;
while i + 3 < bytes.len() {
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
if let Some(close) = find_close(text, i + 2) {
let inner = text[i + 2..close].trim();
if !inner.is_empty() && !out.iter().any(|l| l == inner) {
out.push(inner.to_string());
}
i = close + 2;
continue;
}
}
// Avanza un carácter UTF-8 completo, no un byte.
i += utf8_len(bytes[i]);
}
out
}
/// Posición del `]]` que cierra a partir de `from`, si existe.
fn find_close(text: &str, from: usize) -> Option<usize> {
let bytes = text.as_bytes();
let mut i = from;
while i + 1 < bytes.len() {
if bytes[i] == b']' && bytes[i + 1] == b']' {
return Some(i);
}
// Un `[[` antes del cierre aborta: enlaces anidados no son válidos.
if bytes[i] == b'[' && bytes[i + 1] == b'[' {
return None;
}
i += 1;
}
None
}
/// Largo en bytes del carácter UTF-8 que empieza en `b`.
fn utf8_len(b: u8) -> usize {
match b {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
_ => 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_simple_links() {
let links = parse_links("ver [[Cocina]] y también [[Jardín]].");
assert_eq!(links, vec!["Cocina", "Jardín"]);
}
#[test]
fn trims_inner_whitespace() {
assert_eq!(parse_links("[[ Taller ]]"), vec!["Taller"]);
}
#[test]
fn empty_links_are_dropped() {
assert_eq!(parse_links("[[]] y [[ ]]"), Vec::<String>::new());
}
#[test]
fn duplicates_collapse_to_one() {
assert_eq!(parse_links("[[A]] [[B]] [[A]]"), vec!["A", "B"]);
}
#[test]
fn unclosed_bracket_is_ignored() {
assert_eq!(parse_links("texto [[sin cerrar"), Vec::<String>::new());
}
#[test]
fn handles_unicode_content_around_links() {
let links = parse_links("café ☕ con [[Niños]] — añoño");
assert_eq!(links, vec!["Niños"]);
}
}