p1-3: update kb-parse-md callers + drop BlockView projection in snapshots
Mechanical sweep over `Inline::Text(_)` / `Code(_)` / `Strong(_)` / `Emph(_)`
construction and match sites under the new struct-variant shape introduced
in the previous commit. `Inline::Link { text, href }` is unchanged.
The snapshot test in `tests/blocks_snapshots.rs` previously projected
`ParsedBlock` into a `BlockView`/`PayloadView` shim because the old
`Inline` could not serialize. With the schema fix in place we now
serialize `ParsedBlock` directly through serde — the shim and its
`flatten_inline` helper are removed. Inlines surface as structured
objects (`{"kind":"text","text":"…"}` etc.). Regenerated
`nested-headings.blocks.snapshot.json` to reflect the new shape via
the existing `--ignored` emitter; `code-and-table.blocks.snapshot.json`
has no inlines and is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,12 +300,12 @@ impl InlineBuf {
|
||||
|
||||
fn push_text(&mut self, s: &str) {
|
||||
self.text.push_str(s);
|
||||
self.push_inline(Inline::Text(s.to_string()));
|
||||
self.push_inline(Inline::Text { text: s.to_string() });
|
||||
}
|
||||
|
||||
fn push_code(&mut self, s: &str) {
|
||||
self.text.push_str(s);
|
||||
self.push_inline(Inline::Code(s.to_string()));
|
||||
self.push_inline(Inline::Code { code: s.to_string() });
|
||||
}
|
||||
|
||||
fn open_strong(&mut self) {
|
||||
@@ -313,7 +313,7 @@ impl InlineBuf {
|
||||
}
|
||||
fn close_strong(&mut self) {
|
||||
if let Some(InlineFrame::Strong(kids)) = self.stack.pop() {
|
||||
self.push_inline(Inline::Strong(kids));
|
||||
self.push_inline(Inline::Strong { children: kids });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ impl InlineBuf {
|
||||
}
|
||||
fn close_emph(&mut self) {
|
||||
if let Some(InlineFrame::Emph(kids)) = self.stack.pop() {
|
||||
self.push_inline(Inline::Emph(kids));
|
||||
self.push_inline(Inline::Emph { children: kids });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,8 +361,8 @@ impl InlineBuf {
|
||||
// If formatting tags were unbalanced we close them defensively.
|
||||
while self.stack.len() > 1 {
|
||||
match self.stack.pop().unwrap() {
|
||||
InlineFrame::Strong(kids) => self.push_inline(Inline::Strong(kids)),
|
||||
InlineFrame::Emph(kids) => self.push_inline(Inline::Emph(kids)),
|
||||
InlineFrame::Strong(kids) => self.push_inline(Inline::Strong { children: kids }),
|
||||
InlineFrame::Emph(kids) => self.push_inline(Inline::Emph { children: kids }),
|
||||
InlineFrame::Link { href, text, kids } => {
|
||||
let flat = if !text.is_empty() {
|
||||
text
|
||||
@@ -475,10 +475,11 @@ fn flatten_inlines_to_text(inlines: &[Inline]) -> String {
|
||||
|
||||
fn flatten_one(i: &Inline, out: &mut String) {
|
||||
match i {
|
||||
Inline::Text(s) | Inline::Code(s) => out.push_str(s),
|
||||
Inline::Text { text } => out.push_str(text),
|
||||
Inline::Code { code } => out.push_str(code),
|
||||
Inline::Link { text, .. } => out.push_str(text),
|
||||
Inline::Strong(v) | Inline::Emph(v) => {
|
||||
for c in v {
|
||||
Inline::Strong { children } | Inline::Emph { children } => {
|
||||
for c in children {
|
||||
flatten_one(c, out);
|
||||
}
|
||||
}
|
||||
@@ -823,7 +824,7 @@ impl<'a> WalkState<'a> {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push_str(t);
|
||||
inlines.push(Inline::Text(t.clone()));
|
||||
inlines.push(Inline::Text { text: t.clone() });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -921,7 +922,7 @@ impl<'a> WalkState<'a> {
|
||||
source_span: self.span_for(&range),
|
||||
payload: ParsedPayload::Paragraph {
|
||||
text: raw.clone(),
|
||||
inlines: vec![Inline::Text(raw)],
|
||||
inlines: vec![Inline::Text { text: raw }],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -1477,7 +1478,7 @@ mod tests {
|
||||
assert!(
|
||||
matches!(
|
||||
inl,
|
||||
Inline::Text(_) | Inline::Code(_) | Inline::Link { .. } | Inline::Strong(_) | Inline::Emph(_)
|
||||
Inline::Text { .. } | Inline::Code { .. } | Inline::Link { .. } | Inline::Strong { .. } | Inline::Emph { .. }
|
||||
),
|
||||
"unexpected inline kind: {:?}",
|
||||
inl
|
||||
@@ -1736,11 +1737,11 @@ mod tests {
|
||||
match &blocks[0].payload {
|
||||
ParsedPayload::Paragraph { inlines, .. } => {
|
||||
let kinds: Vec<&'static str> = inlines.iter().map(|i| match i {
|
||||
Inline::Text(_) => "Text",
|
||||
Inline::Code(_) => "Code",
|
||||
Inline::Text { .. } => "Text",
|
||||
Inline::Code { .. } => "Code",
|
||||
Inline::Link { .. } => "Link",
|
||||
Inline::Strong(_) => "Strong",
|
||||
Inline::Emph(_) => "Emph",
|
||||
Inline::Strong { .. } => "Strong",
|
||||
Inline::Emph { .. } => "Emph",
|
||||
}).collect();
|
||||
assert!(kinds.contains(&"Strong"));
|
||||
assert!(kinds.contains(&"Emph"));
|
||||
|
||||
@@ -4,19 +4,13 @@
|
||||
//! below. `body_offset_lines = 1` is used for both fixtures (no
|
||||
//! frontmatter, body starts at file line 1).
|
||||
//!
|
||||
//! Note on snapshot shape: `kb_core::Inline` carries a `serde(tag = "kind")`
|
||||
//! enum representation that cannot serialize newtype variants holding a
|
||||
//! primitive (`Inline::Text(String)` etc.) — that's a serde limitation, not
|
||||
//! ours, and is fixed up in a later kb-core task. To keep the snapshot
|
||||
//! human-readable (and stable across that future fix), we project each
|
||||
//! `ParsedBlock` into a `BlockView` that flattens inline content to plain
|
||||
//! strings before serialization. This still pins the *contract* that
|
||||
//! matters for P1-3: heading paths, source spans, payload kinds, payload
|
||||
//! text content, table headers/rows, and code lang/body.
|
||||
//! Following the kb_core::Inline schema migration (struct-variant shape),
|
||||
//! `ParsedBlock` now serializes directly through serde — no projection
|
||||
//! shim is required. Inlines surface as structured objects, e.g.
|
||||
//! `[{"kind":"text","text":"…"},{"kind":"code","code":"…"}]`.
|
||||
|
||||
use kb_core::{Inline, SourceSpan};
|
||||
use kb_parse_md::parse_blocks;
|
||||
use kb_parse_types::{ParsedBlock, ParsedPayload, Warning};
|
||||
use kb_parse_types::{ParsedBlock, Warning};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
@@ -24,130 +18,10 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Snapshot {
|
||||
blocks: Vec<BlockView>,
|
||||
blocks: Vec<ParsedBlock>,
|
||||
warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BlockView {
|
||||
kind: String,
|
||||
heading_path: Vec<String>,
|
||||
source_span: SourceSpan,
|
||||
payload: PayloadView,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
enum PayloadView {
|
||||
Heading {
|
||||
level: u8,
|
||||
text: String,
|
||||
},
|
||||
Paragraph {
|
||||
text: String,
|
||||
inlines_flat: String,
|
||||
},
|
||||
List {
|
||||
ordered: bool,
|
||||
items_flat: Vec<String>,
|
||||
},
|
||||
Code {
|
||||
lang: Option<String>,
|
||||
code: String,
|
||||
},
|
||||
Table {
|
||||
headers: Vec<String>,
|
||||
rows: Vec<Vec<String>>,
|
||||
},
|
||||
Quote {
|
||||
text: String,
|
||||
inlines_flat: String,
|
||||
},
|
||||
ImageRef {
|
||||
src: String,
|
||||
alt: String,
|
||||
},
|
||||
AudioRef {
|
||||
src: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn flatten_inline(i: &Inline, out: &mut String) {
|
||||
match i {
|
||||
Inline::Text(s) | Inline::Code(s) => out.push_str(s),
|
||||
Inline::Link { text, href } => {
|
||||
out.push('[');
|
||||
out.push_str(text);
|
||||
out.push_str("](");
|
||||
out.push_str(href);
|
||||
out.push(')');
|
||||
}
|
||||
Inline::Strong(v) => {
|
||||
out.push_str("**");
|
||||
for c in v {
|
||||
flatten_inline(c, out);
|
||||
}
|
||||
out.push_str("**");
|
||||
}
|
||||
Inline::Emph(v) => {
|
||||
out.push('*');
|
||||
for c in v {
|
||||
flatten_inline(c, out);
|
||||
}
|
||||
out.push('*');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten(inlines: &[Inline]) -> String {
|
||||
let mut out = String::new();
|
||||
for i in inlines {
|
||||
flatten_inline(i, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn block_to_view(b: &ParsedBlock) -> BlockView {
|
||||
let kind = format!("{:?}", b.kind).to_lowercase();
|
||||
let payload = match &b.payload {
|
||||
ParsedPayload::Heading { level, text } => PayloadView::Heading {
|
||||
level: *level,
|
||||
text: text.clone(),
|
||||
},
|
||||
ParsedPayload::Paragraph { text, inlines } => PayloadView::Paragraph {
|
||||
text: text.clone(),
|
||||
inlines_flat: flatten(inlines),
|
||||
},
|
||||
ParsedPayload::List { ordered, items } => PayloadView::List {
|
||||
ordered: *ordered,
|
||||
items_flat: items.iter().map(|it| flatten(it)).collect(),
|
||||
},
|
||||
ParsedPayload::Code { lang, code } => PayloadView::Code {
|
||||
lang: lang.clone(),
|
||||
code: code.clone(),
|
||||
},
|
||||
ParsedPayload::Table { headers, rows } => PayloadView::Table {
|
||||
headers: headers.clone(),
|
||||
rows: rows.clone(),
|
||||
},
|
||||
ParsedPayload::Quote { text, inlines } => PayloadView::Quote {
|
||||
text: text.clone(),
|
||||
inlines_flat: flatten(inlines),
|
||||
},
|
||||
ParsedPayload::ImageRef { src, alt } => PayloadView::ImageRef {
|
||||
src: src.clone(),
|
||||
alt: alt.clone(),
|
||||
},
|
||||
ParsedPayload::AudioRef { src } => PayloadView::AudioRef { src: src.clone() },
|
||||
};
|
||||
BlockView {
|
||||
kind,
|
||||
heading_path: b.heading_path.clone(),
|
||||
source_span: b.source_span.clone(),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixtures_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
@@ -162,7 +36,7 @@ fn assert_snapshot(fixture: &str, baseline: &str) {
|
||||
|
||||
let (blocks, warns) = parse_blocks(&bytes, 1).unwrap();
|
||||
let snap = Snapshot {
|
||||
blocks: blocks.iter().map(block_to_view).collect(),
|
||||
blocks,
|
||||
warnings: warns,
|
||||
};
|
||||
let actual: Value = serde_json::to_value(&snap).unwrap();
|
||||
@@ -211,7 +85,7 @@ fn emit_blocks_snapshots() {
|
||||
let bytes = fs::read(dir.join(fixture)).unwrap();
|
||||
let (blocks, warns) = parse_blocks(&bytes, 1).unwrap();
|
||||
let snap = Snapshot {
|
||||
blocks: blocks.iter().map(block_to_view).collect(),
|
||||
blocks,
|
||||
warnings: warns,
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&snap).unwrap();
|
||||
@@ -227,14 +101,10 @@ fn snapshot_is_deterministic_across_runs() {
|
||||
let bytes = fs::read(dir.join("nested-headings.md")).unwrap();
|
||||
let (a_blocks, a_warns) = parse_blocks(&bytes, 1).unwrap();
|
||||
let (b_blocks, b_warns) = parse_blocks(&bytes, 1).unwrap();
|
||||
// Compare via the view (which is fully serializable) and via the
|
||||
// structural equality on `ParsedBlock` itself (no serde involved).
|
||||
assert_eq!(a_blocks, b_blocks);
|
||||
assert_eq!(a_warns, b_warns);
|
||||
let av: Vec<_> = a_blocks.iter().map(block_to_view).collect();
|
||||
let bv: Vec<_> = b_blocks.iter().map(block_to_view).collect();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&av).unwrap(),
|
||||
serde_json::to_value(&bv).unwrap()
|
||||
serde_json::to_value(&a_blocks).unwrap(),
|
||||
serde_json::to_value(&b_blocks).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
"payload": {
|
||||
"kind": "paragraph",
|
||||
"text": "intro",
|
||||
"inlines_flat": "intro"
|
||||
"inlines": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "intro"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,7 +65,12 @@
|
||||
"payload": {
|
||||
"kind": "paragraph",
|
||||
"text": "body of A",
|
||||
"inlines_flat": "body of A"
|
||||
"inlines": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "body of A"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -95,7 +105,12 @@
|
||||
"payload": {
|
||||
"kind": "paragraph",
|
||||
"text": "deeper",
|
||||
"inlines_flat": "deeper"
|
||||
"inlines": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "deeper"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -128,7 +143,12 @@
|
||||
"payload": {
|
||||
"kind": "paragraph",
|
||||
"text": "body of B",
|
||||
"inlines_flat": "body of B"
|
||||
"inlines": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "body of B"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user