use crate::tiling::{KnowledgeTile, TileRegistry};
#[derive(Debug, Clone)]
pub enum JumpResult<'a> {
Found(&'a KnowledgeTile),
NotFound { anchor: String, suggestions: Vec<String> },
NoAnchors,
}
pub fn extract_anchors(input: &str) -> Vec<String> {
let mut anchors = Vec::new();
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' {
let start = i + 1;
let mut end = start;
while end < chars.len() && chars[end] != ']' {
end += 1;
}
if end < chars.len() {
let word: String = chars[start..end].iter().collect();
let after = end + 1;
let is_md_link = after < chars.len() && chars[after] == '(';
if !is_md_link
&& !word.is_empty()
&& word.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
anchors.push(word);
}
i = end + 1;
continue;
}
}
i += 1;
}
anchors
}
pub fn jump_context<'a>(input: &str, registry: &'a TileRegistry) -> JumpResult<'a> {
let anchors = extract_anchors(input);
if anchors.is_empty() {
return JumpResult::NoAnchors;
}
let anchor = &anchors[0];
if let Some(tile) = registry.get_by_anchor(anchor) {
return JumpResult::Found(tile);
}
let lower = anchor.to_lowercase();
let suggestions: Vec<String> = registry
.all()
.iter()
.filter(|t| {
let ta = t.anchor.to_lowercase();
ta.starts_with(&lower[..lower.len().min(4)]) || lower.starts_with(&ta[..ta.len().min(4)])
})
.map(|t| t.anchor.clone())
.take(5)
.collect();
JumpResult::NotFound {
anchor: anchor.clone(),
suggestions,
}
}
pub fn jump_all_contexts<'a>(input: &str, registry: &'a TileRegistry) -> Vec<&'a KnowledgeTile> {
let anchors = extract_anchors(input);
let mut seen = std::collections::HashSet::new();
let mut tiles = Vec::new();
for anchor in &anchors {
if seen.insert(anchor.clone()) {
if let Some(tile) = registry.get_by_anchor(anchor) {
tiles.push(tile);
}
}
}
tiles
}
pub fn expand_anchors(input: &str, registry: &TileRegistry) -> String {
let anchors = extract_anchors(input);
let mut result = input.to_string();
for anchor in anchors {
if let Some(tile) = registry.get_by_anchor(&anchor) {
let placeholder = format!("[{}]", anchor);
let expansion = format!(
"[{}]\n\n> **Expanded tile: {}**\n> {}\n",
anchor,
tile.header.trim_start_matches('#').trim(),
tile.body.lines().next().unwrap_or("").trim(),
);
result = result.replacen(&placeholder, &expansion, 1);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tiling::TileRegistry;
const DOC: &str = "## PaymentFlow\nHandles payment initiation.\n\n## Settlement\nClears funds.\n\n## RefundPolicy\nRefunds the original charge.\n";
#[test]
fn test_extract_anchors() {
let anchors = extract_anchors("please review [PaymentFlow] and [Settlement]");
assert_eq!(anchors, vec!["PaymentFlow", "Settlement"]);
}
#[test]
fn test_extract_anchors_skips_md_links() {
let anchors = extract_anchors("[click here](https://example.com) vs [PaymentFlow]");
assert_eq!(anchors, vec!["PaymentFlow"]);
}
#[test]
fn test_jump_context_found() {
let reg = TileRegistry::parse(DOC);
let result = jump_context("review [PaymentFlow] now", ®);
assert!(matches!(result, JumpResult::Found(t) if t.anchor == "PaymentFlow"));
}
#[test]
fn test_jump_context_not_found_with_suggestions() {
let reg = TileRegistry::parse(DOC);
let result = jump_context("review [Payment]", ®);
match result {
JumpResult::NotFound { anchor, suggestions } => {
assert_eq!(anchor, "Payment");
assert!(!suggestions.is_empty());
}
_ => panic!("expected NotFound"),
}
}
#[test]
fn test_jump_all() {
let reg = TileRegistry::parse(DOC);
let tiles = jump_all_contexts("[PaymentFlow] and [Settlement]", ®);
assert_eq!(tiles.len(), 2);
}
#[test]
fn test_expand_anchors() {
let reg = TileRegistry::parse(DOC);
let expanded = expand_anchors("Apply [PaymentFlow] rules.", ®);
assert!(expanded.contains("Expanded tile: PaymentFlow"));
}
}