From 0583bc077c3c985d2c381c7d9cdfc27908cd36cd Mon Sep 17 00:00:00 2001 From: landon Date: Wed, 16 Sep 2026 14:18:57 -0500 Subject: [PATCH] feat: Add aliases support to Story Bible for flexible linking - Add aliases field to StoryBibleItem struct - Add items_by_alias HashMap to StoryBibleIndex for quick alias lookup - Add find_by_alias() method and update find_by_partial_name() to search aliases - Parse aliases from YAML frontmatter (list and comma-separated formats) - Update find_references() to match aliases in addition to IDs and names - Fix @mention pattern matching to properly stop at spaces - Add comprehensive tests for aliases functionality - Update README.md with aliases documentation and usage examples This allows Story Bible entries to have alternative names that can be referenced in draft text using wiki-style [[...]] or mention-style @ syntax. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- README.md | 20 +++- src/story_bible.rs | 236 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 236 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 528668e..32ec094 100644 --- a/README.md +++ b/README.md @@ -383,13 +383,27 @@ location, lore, and other reference directories. It supports: * **Purple background highlighting** of Story Bible references in the editor * Click items in the Story Bible window to see all references to them in the current file * Click reference buttons to jump directly to that location in the editor -* Metadata extraction from YAML frontmatter (id, title, type fields) or from filenames +* Metadata extraction from YAML frontmatter (id, title, type, **aliases** fields) or from filenames * Case-insensitive search and lookup +* **Aliases support**: References match item IDs, names, **and aliases** for flexible linking Character sheets in the `Characters` folder are automatically indexed, and any markdown file with YAML frontmatter containing `id:` and `title:` fields will be -included in the index. References in your draft text that match Story Bible items -are highlighted and clickable. +included in the index. You can also add an `aliases:` field with alternative names +for a character or item: + +```yaml +--- +id: john_smith +title: "John Smith" +type: character +aliases: ["Johnny", "JS"] +--- +``` + +With aliases defined, references like `[[Johnny]]` or `@JS` will match the "John Smith" +character entry. References in your draft text that match Story Bible items +by ID, name, or alias are highlighted and clickable. ### Story Bible Editor (Edit ▸ Story Bible…) diff --git a/src/story_bible.rs b/src/story_bible.rs index cafed25..f768d78 100644 --- a/src/story_bible.rs +++ b/src/story_bible.rs @@ -33,6 +33,8 @@ pub struct StoryBibleItem { pub id: String, /// The item's type (character, location, lore, etc.). pub item_type: String, + /// Alternative names for this item (from aliases field in YAML frontmatter). + pub aliases: Vec, /// Line in the file where the name/ID is defined. pub definition_line: usize, } @@ -44,6 +46,8 @@ pub struct StoryBibleIndex { pub items_by_id: HashMap, /// Map from lowercase name to item info. pub items_by_name: HashMap, + /// Map from lowercase alias to item info. + pub items_by_alias: HashMap, } impl StoryBibleIndex { @@ -52,6 +56,7 @@ impl StoryBibleIndex { Self { items_by_id: HashMap::new(), items_by_name: HashMap::new(), + items_by_alias: HashMap::new(), } } @@ -93,7 +98,13 @@ impl StoryBibleIndex { // Index by name (lowercase for case-insensitive lookup) index .items_by_name - .insert(item.name.to_lowercase(), item); + .insert(item.name.to_lowercase(), item.clone()); + // Index by aliases (lowercase for case-insensitive lookup) + for alias in &item.aliases { + index + .items_by_alias + .insert(alias.to_lowercase(), item.clone()); + } } } } @@ -111,12 +122,23 @@ impl StoryBibleIndex { self.items_by_name.get(&name.to_lowercase()) } - /// Find all items matching a partial name. + /// Find an item by one of its aliases. + pub fn find_by_alias(&self, alias: &str) -> Option<&StoryBibleItem> { + self.items_by_alias.get(&alias.to_lowercase()) + } + + /// Find all items matching a partial name or alias. pub fn find_by_partial_name(&self, query: &str) -> Vec<&StoryBibleItem> { let query_lower = query.to_lowercase(); self.items_by_name .values() - .filter(|item| item.name.to_lowercase().contains(&query_lower)) + .filter(|item| { + item.name.to_lowercase().contains(&query_lower) + || item + .aliases + .iter() + .any(|a| a.to_lowercase().contains(&query_lower)) + }) .collect() } @@ -152,6 +174,7 @@ fn extract_story_bible_item(relative_path: &str, content: &str) -> Option Option = None; let mut id: Option = None; let mut item_type: Option = None; + let mut aliases: Vec = Vec::new(); for line in &lines[start + 1..end] { let trimmed = line.trim(); @@ -183,6 +207,10 @@ fn extract_from_frontmatter(relative_path: &str, content: &str) -> Option Option Vec { + if value.is_empty() { + return Vec::new(); + } + + // Check if it's a YAML list: ["...", "..."] + if value.trim().starts_with('[') && value.trim().ends_with(']') { + let inner = value.trim()[1..value.trim().len() - 1].trim(); + // Parse items from the list + let mut aliases = Vec::new(); + let mut in_quote = false; + let mut quote_char = '\0'; + let mut current = String::new(); + + for c in inner.chars() { + match c { + '"' | '\'' => { + if in_quote && c == quote_char { + in_quote = false; + quote_char = '\0'; + if !current.is_empty() { + aliases.push(current.clone()); + current.clear(); + } + } else if !in_quote { + in_quote = true; + quote_char = c; + } + } + ',' => { + if !in_quote { + if !current.is_empty() { + aliases.push(current.clone()); + current.clear(); + } + } else { + current.push(c); + } + } + _ => { + if in_quote { + current.push(c); + } + } + } + } + + // Add the last item + if !current.is_empty() { + aliases.push(current); + } + + return aliases; + } + + // Comma-separated format + value + .split(',') + .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + /// Find all Story Bible references in a text. /// /// Looks for patterns like `[[Character Name]]` or `@CharacterName`. @@ -247,9 +342,10 @@ pub fn find_references(text: &str, index: &StoryBibleIndex) -> Vec Vec