- 99
pub requirement_ids: Vec<String>, - 100
#[serde(default)] - 101
pub evidence_receipt_ids: Vec<String>, - 102
#[serde(default)] - 103
pub evidence: Vec<vak_intent::EvidenceReceipt>, - 104
#[serde(default, skip_serializing_if = "Option::is_none")] - 105
pub human_review: Option<String>, - 106
} - 107
- 108
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 109
#[serde(tag = "type", rename_all = "snake_case")] - 110
pub enum OutputContent { - 111
Document { - 112
document: PresentationDocument, - 113
}, - 114
Information { - 115
label: String, - 116
detail: Option<String>, - 117
}, - 118
Approval { - 119
request_id: String, - 120
tool: String, - 121
args_json: String, - 122
reason: String, - 123
expires_at: Option<String>, - 124
}, - 125
Progress { - 126
label: String, - 127
detail: Option<String>, - 128
percent: Option<u8>, - 129
}, - 130
Retry { - 131
attempt: u32, - 132
delay_ms: u64, - 133
reason: String, - 134
}, - 135
Error { - 136
message: String, - 137
source: Option<String>, - 138
retryable: bool, - 139
}, - 140
Outcome { - 141
summary: String, - 142
document: Option<PresentationDocument>, - 143
}, - 144
Artifact { - 145
artifact: ArtifactRef, - 146
}, - 147
/// Validated data supplied by an installed presentation skill. The - 148
/// renderer registry, never the model, decides how this is displayed. - 149
Structured { - 150
output: crate::skills::StructuredOutput, - 151
}, - 152
/// A host-compiled adaptive tree. The original fallback remains attached - 153
/// so constrained clients and voice narration can lower it safely. - 154
Adaptive { - 155
tree: vak_presentation::RenderTree, - 156
fallback_text: String, - 157
}, - 158
} - 159
- 160
impl OutputContent { - 161
/// Build the canonical adaptive content envelope for a specialized - 162
/// renderer. Degraded compilations deliberately remain on the caller's - 163
/// existing document path so the original renderer stays authoritative. - 164
pub fn from_compiled_adaptive( - 165
compiled: vak_presentation::CompiledPresentation, - 166
fallback_text: impl Into<String>, - 167
) -> Option<Self> { - 168
match compiled { - 169
vak_presentation::CompiledPresentation::Rich(tree) => Some(Self::Adaptive { - 170
fallback_text: fallback_text.into(), - 171
tree, - 172
}), - 173
vak_presentation::CompiledPresentation::Fallback { .. } => None, - 174
} - 175
} - 176
} - 177
- 178
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 179
pub struct OutputTimeline { - 180
pub schema_version: u16, - 181
pub session_id: String, - 182
#[serde(default)] - 183
pub cursor: Option<String>, - 184
pub items: Vec<OutputItem>, - 185
#[serde(default)] - 186
pub diagnostics: Vec<String>, - 187
#[serde(default, skip_serializing_if = "Option::is_none")] - 188
pub goal: Option<vak_intent::GoalState>, - 189
} - 190
- 191
impl OutputTimeline { - 192
pub fn empty(session_id: impl Into<String>) -> Self { - 193
Self { - 194
schema_version: PRESENTATION_SCHEMA_VERSION, - 195
session_id: session_id.into(), - 196
cursor: None, - 197
items: Vec::new(), - 198
diagnostics: Vec::new(), - 199
goal: None, - 200
} - 201
} - 202
} - 203
- 204
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 205
#[serde(tag = "type", rename_all = "snake_case")] - 206
pub enum OutputStreamEvent { - 207
Snapshot { - 208
timeline: OutputTimeline, - 209
}, - 210
ItemStarted { - 211
item: OutputItem, - 212
}, - 213
TextDelta { - 214
item_id: String, - 215
delta: String, - 216
text: String, - 217
}, - 218
ItemReplaced { - 219
item: OutputItem, - 220
}, - 221
ItemCompleted { - 222
item_id: String, - 223
status: OutputStatus, - 224
}, - 225
} - 226
- 227
/// A reconnectable presentation frame. An ordinary live frame carries only - 228
/// `delta`, which a consumer applies to its own running `OutputTimeline` - 229
/// (mirroring the server's own `apply_stream_event`); `snapshot` is present - 230
/// only on the initial connect frame, run settlement, and an explicit - 231
/// resync, where it is the sole authoritative replacement and `delta` is - 232
/// `None`. A frame never carries both: the full-timeline-per-live-event - 233
/// payload this replaces measured 0.6-9.3 MB per answer (docs/audits). - 234
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 235
pub struct OutputStreamFrame { - 236
pub sequence: Option<u64>, - 237
pub delta: Option<OutputStreamEvent>, - 238
#[serde(default, skip_serializing_if = "Option::is_none")] - 239
pub snapshot: Option<OutputTimeline>, - 240
} - 241
- 242
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 243
pub struct SurfaceCapabilities { - 244
pub structured_blocks: bool, - 245
pub tables: bool, - 246
pub code: bool, - 247
pub links: bool, - 248
pub media: bool, - 249
pub file_references: bool, - 250
pub actions: bool, - 251
pub color: bool, - 252
pub interactive: bool, - 253
pub accessible_plain: bool, - 254
pub max_chars: Option<usize>, - 255
} - 256
- 257
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 258
pub struct PresentationDocument { - 259
pub schema_version: u16, - 260
pub source_markdown: String, - 261
pub blocks: Vec<DocumentBlock>, - 262
#[serde(default)] - 263
pub coverage: Vec<DocumentCoverage>, - 264
#[serde(default)] - 265
pub metadata: BTreeMap<String, String>, - 266
#[serde(default)] - 267
pub diagnostics: Vec<String>, - 268
} - 269
- 270
impl Default for PresentationDocument { - 271
fn default() -> Self { - 272
Self { - 273
schema_version: PRESENTATION_SCHEMA_VERSION, - 274
source_markdown: String::new(), - 275
blocks: Vec::new(), - 276
coverage: Vec::new(), - 277
metadata: BTreeMap::new(), - 278
diagnostics: Vec::new(), - 279
} - 280
} - 281
} - 282
- 283
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 284
pub struct DocumentCoverage { - 285
pub block_id: String, - 286
pub disposition: DocumentCoverageDisposition, - 287
#[serde(default, skip_serializing_if = "Option::is_none")] - 288
pub diagnostic: Option<String>, - 289
} - 290
- 291
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 292
#[serde(rename_all = "snake_case")] - 293
pub enum DocumentCoverageDisposition { - 294
Native, - 295
Fallback, - 296
} - 297
- 298
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 299
#[serde(tag = "type", rename_all = "snake_case")] - 300
pub enum DocumentBlock { - 301
Heading { - 302
id: String, - 303
level: u8, - 304
content: Vec<InlineNode>, - 305
}, - 306
Paragraph { - 307
id: String, - 308
content: Vec<InlineNode>, - 309
}, - 310
List { - 311
id: String, - 312
ordered: bool, - 313
start: Option<u64>, - 314
items: Vec<Vec<DocumentBlock>>, - 315
}, - 316
Table { - 317
id: String, - 318
alignments: Vec<TableAlignment>, - 319
header: Vec<Vec<InlineNode>>, - 320
rows: Vec<Vec<Vec<InlineNode>>>, - 321
}, - 322
Quote { - 323
id: String, - 324
blocks: Vec<DocumentBlock>, - 325
}, - 326
Code { - 327
id: String, - 328
language: Option<String>, - 329
filename: Option<String>, - 330
content: String, - 331
}, - 332
Structured { - 333
id: String, - 334
output: crate::skills::StructuredOutput, - 335
fallback_markdown: String, - 336
}, - 337
Diagram { - 338
id: String, - 339
source: String, - 340
fallback_markdown: String, - 341
}, - 342
Callout { - 343
id: String, - 344
tone: CalloutTone, - 345
title: Option<String>, - 346
blocks: Vec<DocumentBlock>, - 347
}, - 348
Diff { - 349
id: String, - 350
content: String, - 351
}, - 352
Citations { - 353
id: String, - 354
items: Vec<Citation>, - 355
}, - 356
Media { - 357
id: String, - 358
source: String, - 359
alt: String, - 360
media_type: Option<String>, - 361
}, - 362
ArtifactRef { - 363
id: String, - 364
artifact: ArtifactRef, - 365
}, - 366
Rule { - 367
id: String, - 368
}, - 369
RawMarkdown { - 370
id: String, - 371
markdown: String, - 372
reason: String, - 373
}, - 374
} - 375
- 376
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 377
#[serde(tag = "type", rename_all = "snake_case")] - 378
pub enum InlineNode { - 379
Text { - 380
text: String, - 381
}, - 382
Strong { - 383
content: Vec<InlineNode>, - 384
}, - 385
Emphasis { - 386
content: Vec<InlineNode>, - 387
}, - 388
Strikethrough { - 389
content: Vec<InlineNode>, - 390
}, - 391
Code { - 392
code: String, - 393
}, - 394
Link { - 395
label: Vec<InlineNode>, - 396
url: String, - 397
title: Option<String>, - 398
safe: bool, - 399
}, - 400
Image { - 401
alt: String, - 402
url: String, - 403
title: Option<String>, - 404
safe: bool, - 405
}, - 406
SoftBreak, - 407
HardBreak, - 408
RawHtml { - 409
html: String, - 410
}, - 411
} - 412
- 413
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 414
#[serde(rename_all = "snake_case")] - 415
pub enum TableAlignment { - 416
None, - 417
Left, - 418
Center, - 419
Right, - 420
} - 421
- 422
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 423
#[serde(rename_all = "snake_case")] - 424
pub enum CalloutTone { - 425
Information, - 426
Success, - 427
Warning, - 428
Error, - 429
} - 430
- 431
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 432
pub struct Citation { - 433
pub label: String, - 434
pub url: String, - 435
pub title: Option<String>, - 436
} - 437
- 438
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 439
pub struct ArtifactRef { - 440
pub name: String, - 441
pub path: Option<String>, - 442
pub media_type: Option<String>, - 443
pub description: Option<String>, - 444
/// The observed size. A technical detail: a surface shows it only when - 445
/// the person asked for technical details. - 446
#[serde(default, skip_serializing_if = "Option::is_none")] - 447
pub size_bytes: Option<u64>, - 448
/// Where the file stands, derived from durable records. `None` when the - 449
/// projection has no evidence either way, as for a live event before its - 450
/// run settles. - 451
#[serde(default, skip_serializing_if = "Option::is_none")] - 452
pub status: Option<ArtifactStatus>, - 453
} - 454
- 455
/// Where a produced file stands for the person it was made for. Versions - 456
/// count the saved versions of one draft, from 1. - 457
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 458
#[serde(tag = "state", rename_all = "snake_case")] - 459
pub enum ArtifactStatus { - 460
/// Waiting for review; the workspace has not changed. `saved_as` is the - 461
/// newest saved version, absent until the draft is first saved for review. - 462
Draft { - 463
version: u32, - 464
#[serde(default, skip_serializing_if = "Option::is_none")] - 465
saved_as: Option<VersionFile>, - 466
}, - 467
/// A reviewed version was accepted into the workspace. - 468
Accepted { - 469
version: u32, - 470
#[serde(default, skip_serializing_if = "Option::is_none")] - 471
saved_as: Option<VersionFile>, - 472
}, - 473
/// Written straight into the workspace; there was no draft to review. - 474
InFolder, - 475
} - 476
- 477
/// A saved draft version and this file's path inside it. - 478
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 479
pub struct VersionFile { - 480
pub version_id: String, - 481
pub path: String, - 482
} - 483
- 484
#[derive(Debug)] - 485
enum Node { - 486
Root(Vec<Node>), - 487
Paragraph(Vec<Node>), - 488
Heading(u8, Vec<Node>), - 489
BlockQuote(Vec<Node>), - 490
CodeBlock(Option<String>, String), - 491
List(Option<u64>, Vec<Node>), - 492
Item(Vec<Node>), - 493
Table(Vec<TableAlignment>, Vec<Node>), - 494
TableHead(Vec<Node>), - 495
TableRow(Vec<Node>), - 496
TableCell(Vec<Node>), - 497
Strong(Vec<Node>), - 498
Emphasis(Vec<Node>), - 499
Strikethrough(Vec<Node>), - 500
Link(String, Option<String>, Vec<Node>), - 501
Image(String, Option<String>, Vec<Node>), - 502
Text(String), - 503
InlineCode(String), - 504
SoftBreak, - 505
HardBreak, - 506
Rule, - 507
RawHtml(String), - 508
Unsupported(String, Vec<Node>), - 509
} - 510
- 511
impl Node { - 512
fn children_mut(&mut self) -> Option<&mut Vec<Node>> { - 513
match self { - 514
Self::Root(v) - 515
| Self::Paragraph(v) - 516
| Self::Heading(_, v) - 517
| Self::BlockQuote(v) - 518
| Self::List(_, v) - 519
| Self::Item(v) - 520
| Self::Table(_, v) - 521
| Self::TableHead(v) - 522
| Self::TableRow(v) - 523
| Self::TableCell(v) - 524
| Self::Strong(v) - 525
| Self::Emphasis(v) - 526
| Self::Strikethrough(v) - 527
| Self::Link(_, _, v) - 528
| Self::Image(_, _, v) - 529
| Self::Unsupported(_, v) => Some(v), - 530
_ => None, - 531
} - 532
} - 533
} - 534
- 535
/// ``[`X`](X)``, a code span linked to exactly itself, becomes `` `X` ``. - 536
/// The link adds nothing, and when `X` holds a space (an Office citation - 537
/// such as `book.xlsx#'Q3 by region'!B2`) it is not a valid link at all and - 538
/// would otherwise show as raw brackets around the code. - 539
fn unlink_self_linked_code(source: &str) -> std::borrow::Cow<'_, str> { - 540
if !source.contains("[`") { - 541
return std::borrow::Cow::Borrowed(source); - 542
} - 543
let mut out = String::with_capacity(source.len()); - 544
let mut rest = source; - 545
while let Some(start) = rest.find("[`") { - 546
out.push_str(&rest[..start]); - 547
let candidate = &rest[start + 2..]; - 548
let collapsed = candidate.find("`](").and_then(|close| { - 549
let code = &candidate[..close]; - 550
let destination = candidate[close + 3..].strip_prefix(code)?; - 551
(!code.is_empty() && !code.contains('`') && !code.contains('\n')).then_some(())?; - 552
destination - 553
.strip_prefix(')') - 554
.map(|_| (code, close + 3 + code.len() + 1)) - 555
}); - 556
match collapsed { - 557
Some((code, consumed)) => { - 558
out.push('`'); - 559
out.push_str(code); - 560
out.push('`'); - 561
rest = &candidate[consumed..]; - 562
} - 563
None => { - 564
out.push_str("[`"); - 565
rest = candidate; - 566
} - 567
} - 568
} - 569
out.push_str(rest); - 570
std::borrow::Cow::Owned(out) - 571
} - 572
- 573
pub fn compile_markdown(source: impl Into<String>) -> PresentationDocument { - 574
let raw_source = source.into(); - 575
let source_markdown: String = raw_source - 576
.chars() - 577
.filter(|c| !('\u{E0000}'..='\u{E007F}').contains(c)) - 578
.collect(); - 579
let parsed = unlink_self_linked_code(&source_markdown); - 580
let mut stack = vec![Node::Root(Vec::new())]; - 581
let options = Options::ENABLE_TABLES - 582
| Options::ENABLE_STRIKETHROUGH - 583
| Options::ENABLE_TASKLISTS - 584
| Options::ENABLE_FOOTNOTES; - 585
for event in Parser::new_ext(&parsed, options) { - 586
match event { - 587
Event::Start(tag) => stack.push(start_node(tag)), - 588
Event::End(_) => close_node(&mut stack), - 589
Event::Text(text) => append_node(&mut stack, Node::Text(text.into_string())), - 590
Event::Code(code) => append_node(&mut stack, Node::InlineCode(code.into_string())), - 591
Event::Html(html) | Event::InlineHtml(html) => { - 592
append_node(&mut stack, Node::RawHtml(html.into_string())); - 593
} - 594
Event::SoftBreak => append_node(&mut stack, Node::SoftBreak), - 595
Event::HardBreak => append_node(&mut stack, Node::HardBreak), - 596
Event::Rule => append_node(&mut stack, Node::Rule), - 597
Event::TaskListMarker(checked) => append_node( - 598
&mut stack, - 599
Node::Text(if checked { "☑ " } else { "☐ " }.into()), - 600
), - 601
Event::FootnoteReference(label) => { - 602
append_node(&mut stack, Node::Text(format!("[^{label}]"))); - 603
} - 604
Event::InlineMath(value) | Event::DisplayMath(value) => { - 605
append_node(&mut stack, Node::InlineCode(value.into_string())); - 606
} - 607
} - 608
} - 609
while stack.len() > 1 { - 610
close_node(&mut stack); - 611
} - 612
let nodes = match stack.pop() { - 613
Some(Node::Root(nodes)) => nodes, - 614
_ => Vec::new(), - 615
}; - 616
let mut ids = Ids::default(); - 617
let mut diagnostics = Vec::new(); - 618
let blocks = nodes_to_blocks(nodes, &mut ids, &mut diagnostics); - 619
let mut coverage = Vec::new(); - 620
collect_coverage(&blocks, &mut coverage); - 621
PresentationDocument { - 622
schema_version: PRESENTATION_SCHEMA_VERSION, - 623
source_markdown, - 624
blocks, - 625
coverage, - 626
metadata: BTreeMap::new(), - 627
diagnostics, - 628
} - 629
} - 630
- 631
fn collect_coverage(blocks: &[DocumentBlock], coverage: &mut Vec<DocumentCoverage>) { - 632
for block in blocks { - 633
let (block_id, disposition, diagnostic, nested) = match block { - 634
DocumentBlock::Heading { id, .. } - 635
| DocumentBlock::Paragraph { id, .. } - 636
| DocumentBlock::Table { id, .. } - 637
| DocumentBlock::Code { id, .. } - 638
| DocumentBlock::Structured { id, .. } - 639
| DocumentBlock::Diagram { id, .. } - 640
| DocumentBlock::Diff { id, .. } - 641
| DocumentBlock::Citations { id, .. } - 642
| DocumentBlock::Media { id, .. } - 643
| DocumentBlock::ArtifactRef { id, .. } - 644
| DocumentBlock::Rule { id } => (id, DocumentCoverageDisposition::Native, None, None), - 645
DocumentBlock::List { id, items, .. } => { - 646
coverage.push(DocumentCoverage { - 647
block_id: id.clone(), - 648
disposition: DocumentCoverageDisposition::Native, - 649
diagnostic: None, - 650
}); - 651
for item in items { - 652
collect_coverage(item, coverage); - 653
} - 654
continue; - 655
} - 656
DocumentBlock::Quote { id, blocks } | DocumentBlock::Callout { id, blocks, .. } => ( - 657
id, - 658
DocumentCoverageDisposition::Native, - 659
None, - 660
Some(blocks.as_slice()), - 661
), - 662
DocumentBlock::RawMarkdown { id, reason, .. } => ( - 663
id, - 664
DocumentCoverageDisposition::Fallback, - 665
Some(reason.clone()), - 666
None, - 667
), - 668
}; - 669
coverage.push(DocumentCoverage { - 670
block_id: block_id.clone(), - 671
disposition, - 672
diagnostic, - 673
}); - 674
if let Some(nested) = nested { - 675
collect_coverage(nested, coverage); - 676
} - 677
} - 678
} - 679
- 680
fn start_node(tag: Tag<'_>) -> Node { - 681
match tag { - 682
Tag::Paragraph => Node::Paragraph(Vec::new()), - 683
Tag::Heading { level, .. } => Node::Heading(heading_level(level), Vec::new()), - 684
Tag::BlockQuote(_) => Node::BlockQuote(Vec::new()), - 685
Tag::CodeBlock(kind) => Node::CodeBlock( - 686
match kind { - 687
CodeBlockKind::Indented => None, - 688
CodeBlockKind::Fenced(info) => info - 689
.split_whitespace() - 690
.next() - 691
.filter(|value| !value.is_empty()) - 692
.map(ToOwned::to_owned), - 693
}, - 694
String::new(), - 695
), - 696
Tag::List(start) => Node::List(start, Vec::new()), - 697
Tag::Item => Node::Item(Vec::new()), - 698
Tag::Table(alignments) => Node::Table( - 699
alignments - 700
.into_iter() - 701
.map(|alignment| match alignment { - 702
Alignment::None => TableAlignment::None, - 703
Alignment::Left => TableAlignment::Left, - 704
Alignment::Center => TableAlignment::Center, - 705
Alignment::Right => TableAlignment::Right, - 706
}) - 707
.collect(), - 708
Vec::new(), - 709
), - 710
Tag::TableHead => Node::TableHead(Vec::new()), - 711
Tag::TableRow => Node::TableRow(Vec::new()), - 712
Tag::TableCell => Node::TableCell(Vec::new()), - 713
Tag::Emphasis => Node::Emphasis(Vec::new()), - 714
Tag::Strong => Node::Strong(Vec::new()), - 715
Tag::Strikethrough => Node::Strikethrough(Vec::new()), - 716
Tag::Link { - 717
dest_url, title, .. - 718
} => Node::Link( - 719
dest_url.into_string(), - 720
nonempty(title.into_string()), - 721
Vec::new(), - 722
), - 723
Tag::Image { - 724
dest_url, title, .. - 725
} => Node::Image( - 726
dest_url.into_string(), - 727
nonempty(title.into_string()), - 728
Vec::new(), - 729
), - 730
other => Node::Unsupported(format!("{other:?}"), Vec::new()), - 731
} - 732
} - 733
- 734
fn append_node(stack: &mut [Node], node: Node) { - 735
if let Some(children) = stack.last_mut().and_then(Node::children_mut) { - 736
children.push(node); - 737
} else if let Some(Node::CodeBlock(_, content)) = stack.last_mut() - 738
&& let Node::Text(text) = node - 739
{ - 740
content.push_str(&text); - 741
} - 742
} - 743
- 744
fn close_node(stack: &mut Vec<Node>) { - 745
if stack.len() < 2 { - 746
return; - 747
} - 748
let Some(node) = stack.pop() else { - 749
return; - 750
}; - 751
if let Some(Node::CodeBlock(_, parent)) = stack.last_mut() - 752
&& let Node::Text(text) = node - 753
{ - 754
parent.push_str(&text); - 755
return; - 756
} - 757
if let Some(children) = stack.last_mut().and_then(Node::children_mut) { - 758
children.push(node); - 759
} - 760
} - 761
- 762
fn heading_level(level: HeadingLevel) -> u8 { - 763
match level { - 764
HeadingLevel::H1 => 1, - 765
HeadingLevel::H2 => 2, - 766
HeadingLevel::H3 => 3, - 767
HeadingLevel::H4 => 4, - 768
HeadingLevel::H5 => 5, - 769
HeadingLevel::H6 => 6, - 770
} - 771
} - 772
- 773
fn nonempty(value: String) -> Option<String> { - 774
(!value.is_empty()).then_some(value) - 775
} - 776
- 777
#[derive(Default)] - 778
struct Ids(usize); - 779
- 780
impl Ids { - 781
fn next(&mut self) -> String { - 782
self.0 += 1; - 783
format!("block-{}", self.0) - 784
} - 785
} - 786
- 787
fn nodes_to_blocks( - 788
nodes: Vec<Node>, - 789
ids: &mut Ids, - 790
diagnostics: &mut Vec<String>, - 791
) -> Vec<DocumentBlock> { - 792
let mut blocks = Vec::new(); - 793
let mut inline_run = Vec::new(); - 794
for node in nodes { - 795
if matches!( - 796
node, - 797
Node::Text(_) - 798
| Node::Strong(_) - 799
| Node::Emphasis(_) - 800
| Node::Strikethrough(_) - 801
| Node::Link(..) - 802
| Node::Image(..) - 803
| Node::InlineCode(_) - 804
| Node::SoftBreak - 805
| Node::HardBreak - 806
) { - 807
inline_run.push(node); - 808
continue; - 809
} - 810
flush_inline_run(&mut inline_run, &mut blocks, ids); - 811
match node { - 812
Node::Paragraph(children) => { - 813
let content = nodes_to_inline(children); - 814
if !content.is_empty() { - 815
let mut text_buf = String::new(); - 816
for node in &content { - 817
match node { - 818
InlineNode::Text { text } => text_buf.push_str(text), - 819
InlineNode::Code { code } => text_buf.push_str(code), - 820
InlineNode::SoftBreak | InlineNode::HardBreak => text_buf.push('\n'), - 821
InlineNode::Strong { content } - 822
| InlineNode::Emphasis { content } - 823
| InlineNode::Strikethrough { content } => { - 824
text_buf.push_str(&inline_text(content)); - 825
} - 826
_ => {} - 827
} - 828
} - 829
let spans = crate::skills::find_semantic_json_spans(&text_buf); - 830
if let Some((start, end)) = spans.into_iter().next() { - 831
let candidate = &text_buf[start..end]; - 832
if let Ok(output) = crate::skills::parse_fragment(candidate) { - 833
let before = text_buf[..start].trim(); - 834
let clean_before = before - 835
.strip_prefix("vak\n") - 836
.or_else(|| before.strip_prefix("vak ")) - 837
.or_else(|| before.strip_prefix("Vak\n")) - 838
.or_else(|| before.strip_prefix("Vak ")) - 839
.or_else(|| before.strip_prefix("vak")) - 840
.or_else(|| before.strip_prefix("Vak")) - 841
.unwrap_or(before) - 842
.trim(); - 843
if !clean_before.is_empty() { - 844
blocks.push(DocumentBlock::Paragraph { - 845
id: ids.next(), - 846
content: vec![InlineNode::Text { - 847
text: clean_before.to_string(), - 848
}], - 849
}); - 850
} - 851
let fallback_markdown = crate::skills::structured_markdown(&output); - 852
blocks.push(DocumentBlock::Structured { - 853
id: ids.next(), - 854
output, - 855
fallback_markdown, - 856
}); - 857
let after = text_buf[end..].trim(); - 858
if !after.is_empty() { - 859
blocks.push(DocumentBlock::Paragraph { - 860
id: ids.next(), - 861
content: vec![InlineNode::Text { - 862
text: after.to_string(), - 863
}], - 864
}); - 865
} - 866
continue; - 867
} - 868
} - 869
blocks.push(DocumentBlock::Paragraph { - 870
id: ids.next(), - 871
content, - 872
}); - 873
} - 874
} - 875
Node::Heading(level, children) => blocks.push(DocumentBlock::Heading { - 876
id: ids.next(), - 877
level, - 878
content: nodes_to_inline(children), - 879
}), - 880
Node::BlockQuote(children) => blocks.push(DocumentBlock::Quote { - 881
id: ids.next(), - 882
blocks: nodes_to_blocks(children, ids, diagnostics), - 883
}), - 884
Node::CodeBlock(language, content) => { - 885
if language.as_deref() == Some("vak") - 886
|| language.as_deref() == Some("json") - 887
|| content.contains("\"semantic_type\"") - 888
{ - 889
let trimmed = content.trim(); - 890
let candidate = if let Some(rest) = trimmed - 891
.strip_prefix("vak\n") - 892
.or_else(|| trimmed.strip_prefix("vak ")) - 893
.or_else(|| trimmed.strip_prefix("Vak\n")) - 894
.or_else(|| trimmed.strip_prefix("Vak ")) - 895
{ - 896
rest.trim() - 897
} else { - 898
trimmed - 899
}; - 900
if let Ok(output) = crate::skills::parse_fragment(candidate) { - 901
let fallback_markdown = crate::skills::structured_markdown(&output); - 902
blocks.push(DocumentBlock::Structured { - 903
id: ids.next(), - 904
output, - 905
fallback_markdown, - 906
}); - 907
continue; - 908
} - 909
if let Some((s, e)) = crate::skills::find_semantic_json_spans(&content) - 910
.into_iter() - 911
.next() - 912
&& let Ok(output) = crate::skills::parse_fragment(&content[s..e]) - 913
{ - 914
let fallback_markdown = crate::skills::structured_markdown(&output); - 915
blocks.push(DocumentBlock::Structured { - 916
id: ids.next(), - 917
output, - 918
fallback_markdown, - 919
}); - 920
continue; - 921
} - 922
if language.as_deref() == Some("vak") { - 923
diagnostics.push("Structured block parsing suppressed".into()); - 924
continue; - 925
} - 926
} - 927
if language.as_deref() == Some("mermaid") { - 928
blocks.push(DocumentBlock::Diagram { - 929
id: ids.next(), - 930
fallback_markdown: content.clone(), - 931
source: content, - 932
}); - 933
continue; - 934
} - 935
let is_diff = language.as_deref() == Some("diff"); - 936
blocks.push(if is_diff { - 937
DocumentBlock::Diff { - 938
id: ids.next(), - 939
content, - 940
} - 941
} else { - 942
DocumentBlock::Code { - 943
id: ids.next(), - 944
language, - 945
filename: None, - 946
content, - 947
} - 948
}); - 949
} - 950
Node::List(start, children) => { - 951
let items = children - 952
.into_iter() - 953
.filter_map(|child| match child { - 954
Node::Item(item) => Some(nodes_to_blocks(item, ids, diagnostics)), - 955
_ => None, - 956
}) - 957
.collect(); - 958
blocks.push(DocumentBlock::List { - 959
id: ids.next(), - 960
ordered: start.is_some(), - 961
start, - 962
items, - 963
}); - 964
} - 965
Node::Table(alignments, children) => { - 966
let mut header = Vec::new(); - 967
let mut rows = Vec::new(); - 968
for child in children { - 969
match child { - 970
Node::TableHead(cells) => header = table_cells(cells), - 971
Node::TableRow(cells) => rows.push(table_cells(cells)), - 972
_ => {} - 973
} - 974
} - 975
blocks.push(DocumentBlock::Table { - 976
id: ids.next(), - 977
alignments, - 978
header, - 979
rows, - 980
}); - 981
} - 982
Node::Rule => blocks.push(DocumentBlock::Rule { id: ids.next() }), - 983
Node::RawHtml(markdown) => { - 984
diagnostics.push("raw HTML was preserved as inert text".into()); - 985
blocks.push(DocumentBlock::RawMarkdown { - 986
id: ids.next(), - 987
markdown, - 988
reason: "raw HTML is not mounted by native renderers".into(), - 989
}); - 990
} - 991
Node::Unsupported(kind, children) => { - 992
diagnostics.push(format!("unsupported CommonMark node preserved: {kind}")); - 993
blocks.push(DocumentBlock::RawMarkdown { - 994
id: ids.next(), - 995
markdown: inline_text(&nodes_to_inline(children)), - 996
reason: format!("unsupported CommonMark node: {kind}"), - 997
}); - 998
} - 999
other => { - 1000
let inline = nodes_to_inline(vec![other]); - 1001
if !inline.is_empty() { - 1002
blocks.push(DocumentBlock::Paragraph { - 1003
id: ids.next(), - 1004
content: inline, - 1005
}); - 1006
} - 1007
} - 1008
} - 1009
} - 1010
flush_inline_run(&mut inline_run, &mut blocks, ids); - 1011
blocks - 1012
} - 1013
- 1014
fn flush_inline_run(nodes: &mut Vec<Node>, blocks: &mut Vec<DocumentBlock>, ids: &mut Ids) { - 1015
if nodes.is_empty() { - 1016
return; - 1017
} - 1018
let content = nodes_to_inline(std::mem::take(nodes)); - 1019
if !content.is_empty() { - 1020
blocks.push(DocumentBlock::Paragraph { - 1021
id: ids.next(), - 1022
content, - 1023
}); - 1024
} - 1025
} - 1026
- 1027
fn table_cells(nodes: Vec<Node>) -> Vec<Vec<InlineNode>> { - 1028
nodes - 1029
.into_iter() - 1030
.filter_map(|node| match node { - 1031
Node::TableCell(children) => Some(nodes_to_inline(children)), - 1032
_ => None, - 1033
}) - 1034
.collect() - 1035
} - 1036
- 1037
fn nodes_to_inline(nodes: Vec<Node>) -> Vec<InlineNode> { - 1038
nodes - 1039
.into_iter() - 1040
.filter_map(|node| match node { - 1041
Node::Text(text) => Some(InlineNode::Text { text }), - 1042
Node::InlineCode(code) => Some(InlineNode::Code { code }), - 1043
Node::SoftBreak => Some(InlineNode::SoftBreak), - 1044
Node::HardBreak => Some(InlineNode::HardBreak), - 1045
Node::RawHtml(html) => Some(InlineNode::RawHtml { html }), - 1046
Node::Strong(children) => Some(InlineNode::Strong { - 1047
content: nodes_to_inline(children), - 1048
}), - 1049
Node::Emphasis(children) => Some(InlineNode::Emphasis { - 1050
content: nodes_to_inline(children), - 1051
}), - 1052
Node::Strikethrough(children) => Some(InlineNode::Strikethrough { - 1053
content: nodes_to_inline(children), - 1054
}), - 1055
Node::Link(url, title, children) => Some(InlineNode::Link { - 1056
label: nodes_to_inline(children), - 1057
safe: safe_link(&url), - 1058
url, - 1059
title, - 1060
}), - 1061
Node::Image(url, title, children) => Some(InlineNode::Image { - 1062
alt: inline_text(&nodes_to_inline(children)), - 1063
safe: safe_media(&url), - 1064
url, - 1065
title, - 1066
}), - 1067
Node::Paragraph(children) | Node::TableCell(children) | Node::Item(children) => { - 1068
Some(InlineNode::Text { - 1069
text: inline_text(&nodes_to_inline(children)), - 1070
}) - 1071
} - 1072
_ => None, - 1073
}) - 1074
.collect() - 1075
} - 1076
- 1077
pub fn inline_text(nodes: &[InlineNode]) -> String { - 1078
let mut output = String::new(); - 1079
for node in nodes { - 1080
match node { - 1081
InlineNode::Text { text } => output.push_str(text), - 1082
InlineNode::Strong { content } - 1083
| InlineNode::Emphasis { content } - 1084
| InlineNode::Strikethrough { content } => output.push_str(&inline_text(content)), - 1085
InlineNode::Code { code } => output.push_str(code), - 1086
InlineNode::Link { label, .. } => output.push_str(&inline_text(label)), - 1087
InlineNode::Image { alt, .. } => output.push_str(alt), - 1088
InlineNode::SoftBreak | InlineNode::HardBreak => output.push('\n'), - 1089
InlineNode::RawHtml { html } => output.push_str(html), - 1090
} - 1091
} - 1092
output - 1093
} - 1094
- 1095
pub fn safe_link(url: &str) -> bool { - 1096
let lower = url.trim().to_ascii_lowercase(); - 1097
lower.starts_with("https://") - 1098
|| lower.starts_with("http://")
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.