this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: remove id from card syntax; fix blank trim whitespace

Card identity is now an app-layer concern. CardEntry.id removed from
tala-format; tala-cli uses position index as sidecar key. Preamble
#let definitions drop id: params. Card syntax is now:
#card[front][back] #cloze[body] #img_cloze(src: "x")

Also fix insert_blank_wrap to strip whitespace from the selected range
before inserting into #blank[], keeping spaces outside:
" is " -> " #blank[is] "

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+41 -53
+7 -12
crates/tala-cli/src/main.rs
··· 91 91 92 92 let mut total = 0; 93 93 for df in &files { 94 - let ids: Vec<String> = df.cards.iter().map(|c| c.id.clone()).collect(); 94 + // Cards are identified by position index in the file. 95 + let ids: Vec<String> = (0..df.cards.len()).map(|i| i.to_string()).collect(); 95 96 let n_fb = df.cards.iter().filter(|c| matches!(c.kind, CardKind::FrontBack { .. })).count(); 96 97 let n_cloze = df.cards.iter().filter(|c| matches!(c.kind, CardKind::Cloze { .. })).count(); 97 98 let n_img = df.cards.iter().filter(|c| matches!(c.kind, CardKind::ImgCloze { .. })).count(); ··· 99 100 println!("{}", df.typ_path.display()); 100 101 println!(" {} cards (front/back: {n_fb} cloze: {n_cloze} img-cloze: {n_img})", 101 102 df.cards.len()); 102 - 103 - // Duplicate IDs within file. 104 - let mut seen = std::collections::HashSet::new(); 105 - let dupes: Vec<_> = ids.iter().filter(|id| !seen.insert(*id)).collect(); 106 - if !dupes.is_empty() { 107 - eprintln!(" duplicate IDs: {}", dupes.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join(", ")); 108 - } 109 103 110 104 let orphans = df.sidecar.orphaned(&ids); 111 105 let missing = df.sidecar.missing(&ids); ··· 145 139 146 140 let mut queue: Vec<Item> = Vec::new(); 147 141 for (fi, df) in files.iter().enumerate() { 148 - for card in &df.cards { 142 + for (ci, card) in df.cards.iter().enumerate() { 143 + let card_id = ci.to_string(); 149 144 if let Some(tag) = tag_filter { 150 145 if !card.tags.iter().any(|t| t == tag) { 151 146 continue; 152 147 } 153 148 } 154 - match (&card.kind, df.sidecar.get(&card.id)) { 149 + match (&card.kind, df.sidecar.get(&card_id)) { 155 150 (CardKind::FrontBack { .. }, sched) => { 156 151 let fwd = match sched { 157 152 Some(CardSchedule::FrontBack { forward_schedule, .. }) => forward_schedule.clone(), 158 153 _ => None, 159 154 }; 160 155 if fwd.as_ref().map_or(true, is_due) { 161 - queue.push(Item { file_idx: fi, card_id: card.id.clone(), 156 + queue.push(Item { file_idx: fi, card_id: card_id.clone(), 162 157 kind: card.kind.clone(), sub_id: "forward".into(), current: fwd }); 163 158 } 164 159 // TODO: reverse side for dir: bi ··· 172 167 let key = format!("b{}", blank.index); 173 168 let current = blank_schedules.get(&key).cloned(); 174 169 if current.as_ref().map_or(true, is_due) { 175 - queue.push(Item { file_idx: fi, card_id: card.id.clone(), 170 + queue.push(Item { file_idx: fi, card_id: card_id.clone(), 176 171 kind: card.kind.clone(), sub_id: key, current }); 177 172 } 178 173 }
+9 -27
crates/tala-format/src/lib.rs
··· 8 8 9 9 #[derive(Debug, Clone)] 10 10 pub struct CardEntry { 11 - pub id: String, 12 11 pub tags: Vec<String>, 13 12 pub kind: CardKind, 14 13 /// Byte range of the full `#card[...][...]` call in the source. ··· 186 185 let args = args_of(node)?; 187 186 let (named, tags, content_blocks) = collect_args(&args); 188 187 189 - // id is optional — empty string signals "needs auto-assignment" 190 - let id = named.get("id").cloned().unwrap_or_default(); 191 188 if content_blocks.len() < 2 { 192 189 return None; 193 190 } ··· 197 194 }; 198 195 199 196 Some(CardEntry { 200 - id, 201 197 tags, 202 198 span, 203 199 kind: CardKind::FrontBack { ··· 210 206 211 207 fn parse_cloze(node: &LinkedNode<'_>, span: Range<usize>) -> Option<CardEntry> { 212 208 let args = args_of(node)?; 213 - let (named, tags, content_blocks) = collect_args(&args); 209 + let (_, tags, content_blocks) = collect_args(&args); 214 210 215 - let id = named.get("id").cloned().unwrap_or_default(); 216 211 let body_span = content_blocks.first()?.clone(); 217 212 218 213 // Walk the body ContentBlock for #blank[] calls. ··· 221 216 find_blanks(&body_node, &mut blanks); 222 217 223 218 Some(CardEntry { 224 - id, 225 219 tags, 226 220 span, 227 221 kind: CardKind::Cloze { body_span, blanks }, ··· 232 226 let args = args_of(node)?; 233 227 let (named, tags, _) = collect_args(&args); 234 228 235 - let id = named.get("id").cloned().unwrap_or_default(); 236 229 let src = named.get("src")?.clone(); 237 230 238 231 Some(CardEntry { 239 - id, 240 232 tags, 241 233 span, 242 234 kind: CardKind::ImgCloze { src }, ··· 279 271 use super::*; 280 272 281 273 const SAMPLE: &str = r#" 282 - #card(id: "fb-001", tags: ("circuits", "passive"))[ 274 + #card(tags: ("circuits", "passive"))[ 283 275 What is the circuit model of an actual capacitor? 284 276 ][ 285 277 Series: ESR + ESL + ideal C 286 278 ] 287 279 288 - #card(id: "fb-002", dir: "bi")[ 280 + #card(dir: "bi")[ 289 281 $P_"avg"$ 290 282 ][ 291 283 $ P_"avg" = V_"rms"^2 / R $ 292 284 ] 293 285 294 - #cloze(id: "cl-001")[ 286 + #cloze[ 295 287 The resonant frequency is where #blank[inductance (ESL)] and #blank[capacitance (C)] cancel. 296 288 ] 297 289 298 - #img_cloze(id: "ic-001", src: "capacitor-photo") 290 + #img_cloze(src: "capacitor-photo") 299 291 "#; 300 292 301 293 #[test] ··· 305 297 } 306 298 307 299 #[test] 308 - fn front_back_ids() { 309 - let cards = parse_cards(SAMPLE); 310 - assert_eq!(cards[0].id, "fb-001"); 311 - assert_eq!(cards[1].id, "fb-002"); 312 - } 313 - 314 - #[test] 315 300 fn bidirectional_flag() { 316 301 let cards = parse_cards(SAMPLE); 317 302 let CardKind::FrontBack { dir, .. } = &cards[1].kind else { panic!() }; ··· 343 328 344 329 #[test] 345 330 fn spans_are_non_empty() { 346 - for card in parse_cards(SAMPLE) { 347 - assert!(!card.span.is_empty(), "card {} has empty span", card.id); 331 + for (i, card) in parse_cards(SAMPLE).iter().enumerate() { 332 + assert!(!card.span.is_empty(), "card {i} has empty span"); 348 333 } 349 334 } 350 335 351 336 #[test] 352 337 fn span_text_starts_with_card_call() { 353 - // Note: in typst markup, the '#' sigil is *outside* the FuncCall node, 354 - // so spans start with the bare function name, not '#'. 355 - // The caller can recover the '#' via span.start - 1 if needed. 356 338 let cards = parse_cards(SAMPLE); 357 - for card in &cards { 339 + for (i, card) in cards.iter().enumerate() { 358 340 let text = &SAMPLE[card.span.clone()]; 359 341 assert!( 360 342 text.starts_with("card") || text.starts_with("cloze") || text.starts_with("img_cloze"), 361 - "card {} span text: {:?}", card.id, &text[..20.min(text.len())] 343 + "card {i} span text: {:?}", &text[..20.min(text.len())] 362 344 ); 363 345 } 364 346 }
+11 -11
crates/tala-typst/src/lib.rs
··· 270 270 /// visible as plain text (SVG overlay in the editor provides the highlight). 271 271 const PREAMBLE_AUTHORING: &str = r#" 272 272 #set page(width: 300pt, height: auto, margin: 10pt) 273 - #let card(id: "", dir: "fwd", ..args) = { 273 + #let card(dir: "fwd", tags: (), ..args) = { 274 274 let sides = args.pos() 275 275 sides.at(0, default: []) 276 276 if sides.len() > 1 { ··· 279 279 } 280 280 } 281 281 #let blank(..args) = args.pos().at(0, default: []) 282 - #let cloze(id: "", ..args) = args.pos().at(0, default: []) 283 - #let img_cloze(id: "", src: "") = image("images/" + src + ".png") 282 + #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 283 + #let img_cloze(src: "") = image("images/" + src + ".png") 284 284 #let img(name) = image("images/" + name + ".png") 285 285 "#; 286 286 287 287 /// FrontBack: front side only; other card types unchanged. 288 288 const PREAMBLE_REVIEW_FRONT: &str = r#" 289 289 #set page(width: 300pt, height: auto, margin: 10pt) 290 - #let card(id: "", dir: "fwd", ..args) = args.pos().at(0, default: []) 290 + #let card(dir: "fwd", tags: (), ..args) = args.pos().at(0, default: []) 291 291 #let blank(..args) = args.pos().at(0, default: []) 292 - #let cloze(id: "", ..args) = args.pos().at(0, default: []) 293 - #let img_cloze(id: "", src: "") = image("images/" + src + ".png") 292 + #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 293 + #let img_cloze(src: "") = image("images/" + src + ".png") 294 294 #let img(name) = image("images/" + name + ".png") 295 295 "#; 296 296 297 297 /// Cloze review: blank contents replaced by fixed-width underline boxes. 298 298 const PREAMBLE_REVIEW_CLOZE: &str = r#" 299 299 #set page(width: 300pt, height: auto, margin: 10pt) 300 - #let card(id: "", dir: "fwd", ..args) = { 300 + #let card(dir: "fwd", tags: (), ..args) = { 301 301 let sides = args.pos() 302 302 sides.at(0, default: []) 303 303 if sides.len() > 1 { ··· 306 306 } 307 307 } 308 308 #let blank(..args) = box(width: 4em, stroke: (bottom: 0.5pt), inset: (bottom: 2pt))[] 309 - #let cloze(id: "", ..args) = args.pos().at(0, default: []) 310 - #let img_cloze(id: "", src: "") = image("images/" + src + ".png") 309 + #let cloze(tags: (), ..args) = args.pos().at(0, default: []) 310 + #let img_cloze(src: "") = image("images/" + src + ".png") 311 311 #let img(name) = image("images/" + name + ".png") 312 312 "#; 313 313 ··· 453 453 mod tests { 454 454 use super::*; 455 455 456 - const FRONT_BACK: &str = r#"#card(id: "fb-001")[What is $E = m c^2$?][Einstein's mass--energy relation.]"#; 457 - const CLOZE: &str = r#"#cloze(id: "cl-001")[Resonance occurs where #blank[inductance] cancels #blank[capacitance].]"#; 456 + const FRONT_BACK: &str = r#"#card[What is $E = m c^2$?][Einstein's mass--energy relation.]"#; 457 + const CLOZE: &str = r#"#cloze[Resonance occurs where #blank[inductance] cancels #blank[capacitance].]"#; 458 458 459 459 #[test] 460 460 fn render_authoring_front_back() {
+14 -3
crates/tala/src/main.rs
··· 129 129 #[component] 130 130 fn Editor() -> Element { 131 131 let mut source = use_signal(|| { 132 - r#"#cloze(id: "example")[The #blank[Gaussian integral] is $integral_(-infinity)^(infinity) e^(-x^2) dif x = sqrt(pi)$]"# 132 + r#"#cloze[The #blank[Gaussian integral] is $integral_(-infinity)^(infinity) e^(-x^2) dif x = sqrt(pi)$]"# 133 133 .to_string() 134 134 }); 135 135 ··· 352 352 a[0] < b[0] + b[2] && a[0] + a[2] > b[0] && a[1] < b[1] + b[3] && a[1] + a[3] > b[1] 353 353 } 354 354 355 - /// Wrap `source[start..end]` in `#blank[...]`. 355 + /// Wrap `source[start..end]` in `#blank[...]`, keeping surrounding whitespace outside. 356 356 fn insert_blank_wrap(source: &str, start: usize, end: usize) -> String { 357 - format!("{}#blank[{}]{}", &source[..start], &source[start..end], &source[end..]) 357 + let raw = &source[start..end]; 358 + let inner = raw.trim(); 359 + let lead = raw.len() - raw.trim_start().len(); 360 + let trail = raw.len() - raw.trim_end().len(); 361 + format!( 362 + "{}{}#blank[{}]{}{}", 363 + &source[..start], 364 + &source[start..start + lead], 365 + inner, 366 + &source[end - trail..end], 367 + &source[end..], 368 + ) 358 369 } 359 370 360 371