Add summary: to character templates; fix two bugs that were hiding it
Add a summary: field (with a short explanatory comment) to
01_character_template.md, and fill the field in on the two shipped
examples, 01_protagonist.md and 02_antagonist.md. Documented in the
Story Bible README's "Adding a New Character" example and its new
"Automatic Linking in the Draft" section, and in the main README's
Story Bible docs.
While wiring this up against the real bundled template rather than a
hand-written test fixture, found two bugs that were silently defeating
last session's auto-linking feature for every project made from that
template:
- extract_from_frontmatter() only read a title: field for an item's
name, but every bundled Story Bible template — character, location,
lore, object, all of them — writes name:, never title:. Every real
item's display name (and therefore everything matched against it in
prose) was falling back to a garbled, uncapitalized filename guess
instead. Now name: is read (title: still works too, for a sheet that
uses that word instead).
- build_editor_job()'s fast path skipped straight to an unformatted
run whenever there were no grammar/spelling issues and no active
search — without checking whether there was a Story Bible reference
to highlight. A name with no spelling issue of its own (which is
most of them) never got its purple background, and with it no visible
way to find the hover card that depends on it.
Both were invisible in the unit tests added last session because they
exercised find_references() directly against a hand-built index, never
through StoryBibleIndex::build() against real files, and the manual
smoke test happened to use a name ("Bixby") that the spell checker
flagged anyway — which incidentally kept the fast path from short-
circuiting. Added a disk-built-index test and a fast-path regression
test so both stay caught.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+63
-4
@@ -201,6 +201,7 @@ fn extract_from_frontmatter(relative_path: &str, content: &str) -> Option<StoryB
|
||||
}
|
||||
|
||||
let mut name: Option<String> = None;
|
||||
let mut title: Option<String> = None;
|
||||
let mut id: Option<String> = None;
|
||||
let mut item_type: Option<String> = None;
|
||||
let mut aliases: Vec<String> = Vec::new();
|
||||
@@ -209,8 +210,13 @@ fn extract_from_frontmatter(relative_path: &str, content: &str) -> Option<StoryB
|
||||
|
||||
for line in &lines[start + 1..end] {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("title:") {
|
||||
name = Some(trimmed["title:".len()..].trim().trim_matches('"').to_string());
|
||||
// `name:` is what every bundled Story Bible template actually
|
||||
// writes; `title:` is accepted too since a scene/draft file's own
|
||||
// frontmatter uses that word and a sheet may borrow it by habit.
|
||||
if trimmed.starts_with("name:") {
|
||||
name = Some(trimmed["name:".len()..].trim().trim_matches('"').to_string());
|
||||
} else if trimmed.starts_with("title:") {
|
||||
title = Some(trimmed["title:".len()..].trim().trim_matches('"').to_string());
|
||||
} else if trimmed.starts_with("id:") {
|
||||
id = Some(trimmed["id:".len()..].trim().trim_matches('"').to_string());
|
||||
} else if trimmed.starts_with("type:") {
|
||||
@@ -234,8 +240,8 @@ fn extract_from_frontmatter(relative_path: &str, content: &str) -> Option<StoryB
|
||||
// `summary:` wins if a sheet somehow has both.
|
||||
let summary = summary.or(description);
|
||||
|
||||
// Use name or filename as fallback
|
||||
let name = name.unwrap_or_else(|| {
|
||||
// Use name, then title, then the filename as fallback.
|
||||
let name = name.or(title).unwrap_or_else(|| {
|
||||
Path::new(relative_path)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
@@ -526,6 +532,34 @@ Some content here."#;
|
||||
assert_eq!(item.item_type, "character");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontmatter_reads_name_the_way_every_bundled_template_actually_writes_it() {
|
||||
// Every sheet the snowflake template ships — character, location,
|
||||
// lore, object — writes `name:`, never `title:`. A regression check
|
||||
// for the mismatch that made every item's name fall back to a
|
||||
// filename-derived guess instead.
|
||||
let content = r#"---
|
||||
id: bixby_reynolds
|
||||
name: Bixby Reynolds
|
||||
type: protagonist
|
||||
---
|
||||
|
||||
# Bixby Reynolds"#;
|
||||
let item = extract_from_frontmatter("main_characters/01_bixby.md", content).unwrap();
|
||||
assert_eq!(item.name, "Bixby Reynolds");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontmatter_prefers_name_over_title_when_a_sheet_somehow_has_both() {
|
||||
let content = r#"---
|
||||
name: Real Name
|
||||
title: Old Spelling
|
||||
---
|
||||
"#;
|
||||
let item = extract_from_frontmatter("test.md", content).unwrap();
|
||||
assert_eq!(item.name, "Real Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontmatter_summary_feeds_the_hover_card() {
|
||||
let content = r#"---
|
||||
@@ -725,6 +759,31 @@ description: The long version.
|
||||
assert_eq!(&"Bixby walked into the room."[refs[0].start..refs[0].end], "Bixby");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_mention_matches_against_a_real_disk_built_index() {
|
||||
// The hand-built `index_with` above exercises the matching rules;
|
||||
// this instead goes through `StoryBibleIndex::build`, the same path
|
||||
// a real project's character sheet takes, as a check that nothing
|
||||
// is lost crossing that boundary (frontmatter parsing, `id`
|
||||
// lowercasing, and so on).
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"md_manuscript_test_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(
|
||||
dir.join("protagonist.md"),
|
||||
"---\nid: protagonist\nname: Protagonist\nsummary: \"hero\"\nslug: protagonist\ntype: protagonist\n---\n\n# Protagonist\n",
|
||||
)
|
||||
.unwrap();
|
||||
let index = StoryBibleIndex::build(&dir);
|
||||
let refs = find_references("Protagonist stood at the door.", &index);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
assert_eq!(refs.len(), 1);
|
||||
assert_eq!(refs[0].item_id, "protagonist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_mention_respects_word_boundaries() {
|
||||
let index = index_with("bixby", "Bixby", &[]);
|
||||
|
||||
Reference in New Issue
Block a user