first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+401
View File
@@ -0,0 +1,401 @@
//! Parses `.ipynb` (Jupyter) notebooks directly into the [`FormattedText`]
//! representation that Warp's rich-text/notebook renderer consumes.
//!
//! This is **render-only**: it produces a read-only view of the notebook's
//! existing content (markdown cells, code cells, and saved outputs). It does
//! not execute cells or round-trip edits back to the file.
//!
//! Only nbformat v4 is supported. Anything that fails to parse as a v4 notebook
//! returns an [`IpynbError`] so callers can fall back to showing the raw file
//! contents instead of a blank view.
use std::collections::BTreeMap;
use markdown_parser::{
CodeBlockText, FormattedImage, FormattedText, FormattedTextFragment, FormattedTextLine,
parse_markdown, parse_markdown_with_gfm_tables,
};
use serde::Deserialize;
/// The only nbformat major version this converter understands.
const SUPPORTED_NBFORMAT: i64 = 4;
/// Maximum length of a code-block language tag we will emit. Real language
/// names are short; a longer value is treated as untrusted junk and dropped so
/// it cannot bloat every code block.
const MAX_LANGUAGE_TAG_CHARS: usize = 32;
/// Error produced when the input cannot be rendered as a supported notebook.
#[derive(Debug, thiserror::Error)]
pub enum IpynbError {
/// The input was not valid notebook JSON.
#[error("failed to parse notebook JSON: {0}")]
Parse(#[from] serde_json::Error),
/// The notebook used an unsupported nbformat version (only v4 is supported).
#[error(
"unsupported notebook format: nbformat={nbformat:?} (only v{SUPPORTED_NBFORMAT} is supported)"
)]
UnsupportedFormat { nbformat: Option<i64> },
}
/// Convert the JSON contents of a `.ipynb` file into [`FormattedText`].
///
/// `gfm_tables` selects the GFM-table-aware Markdown parser for markdown cells,
/// mirroring the `Buffer::from_markdown` behavior (the caller passes the
/// `MarkdownTables` feature flag state).
///
/// Returns an [`IpynbError`] if the input is not a parseable nbformat v4
/// notebook; callers should fall back to [`raw_fallback_formatted_text`] in that
/// case so the contents are shown verbatim (never a blank view).
pub fn ipynb_to_formatted_text(json: &str, gfm_tables: bool) -> Result<FormattedText, IpynbError> {
let notebook: Notebook = serde_json::from_str(json)?;
// Guard against arbitrary JSON that happens to deserialize into an empty
// notebook: require an explicit, supported nbformat version.
if notebook.nbformat != Some(SUPPORTED_NBFORMAT) {
return Err(IpynbError::UnsupportedFormat {
nbformat: notebook.nbformat,
});
}
let language = notebook.language();
// Convert each cell independently (a 1:1 map with an exact size hint), then
// size the final buffer from the per-cell line counts so it is allocated
// exactly once instead of growing incrementally.
let per_cell: Vec<Vec<FormattedTextLine>> = notebook
.cells
.iter()
.map(|cell| cell_lines(cell, &language, gfm_tables))
.collect();
let total_lines = per_cell.iter().map(Vec::len).sum();
let mut lines = Vec::with_capacity(total_lines);
for lines_for_cell in per_cell {
lines.extend(lines_for_cell);
}
Ok(FormattedText::new_trimmed(lines))
}
/// Build a verbatim [`FormattedText`] fallback for content that is not a
/// parseable notebook (malformed JSON, unsupported nbformat version, etc.). The
/// raw content is placed in a single code block so any Markdown/HTML inside it
/// is shown verbatim, never re-interpreted.
pub fn raw_fallback_formatted_text(content: &str) -> FormattedText {
FormattedText::new_trimmed(vec![FormattedTextLine::CodeBlock(CodeBlockText {
lang: "json".to_string(),
code: content.trim_end_matches('\n').to_string(),
})])
}
/// Convert a single notebook cell into its formatted-text lines.
fn cell_lines(cell: &Cell, language: &str, gfm_tables: bool) -> Vec<FormattedTextLine> {
match cell.cell_type.as_str() {
"markdown" => {
let source = cell.source.to_text();
markdown_block_lines(source.trim_end_matches('\n'), gfm_tables)
}
"code" => {
let source = cell.source.to_text();
let mut lines = code_block_lines(language, source.trim_end_matches('\n'));
lines.extend(cell.outputs.iter().flat_map(output_lines));
lines
}
// Raw cells are passed through verbatim in Jupyter; render them as a
// plain (unhighlighted) code block so their contents can't inject
// unexpected markdown.
"raw" => {
let source = cell.source.to_text();
code_block_lines("", source.trim_end_matches('\n'))
}
// Unknown / future cell types are skipped rather than rendered raw.
_ => Vec::new(),
}
}
/// A markdown cell, parsed into formatted text and separated from surrounding
/// blocks by a line break. Empty cells produce no lines.
fn markdown_block_lines(content: &str, gfm_tables: bool) -> Vec<FormattedTextLine> {
if content.is_empty() {
return Vec::new();
}
let parse_fn = if gfm_tables {
parse_markdown_with_gfm_tables
} else {
parse_markdown
};
// A markdown cell is, by definition, Markdown; parse it once. If parsing
// somehow fails, preserve the content verbatim as a plain line rather than
// dropping it.
let parsed = parse_fn(content).unwrap_or_else(|_| {
FormattedText::new(vec![FormattedTextLine::Line(vec![
FormattedTextFragment::plain_text(content),
])])
});
let mut lines = Vec::from(parsed.lines);
lines.push(FormattedTextLine::LineBreak);
lines
}
/// A code block with the given language tag (empty for none), separated from
/// surrounding blocks by a line break.
fn code_block_lines(language: &str, content: &str) -> Vec<FormattedTextLine> {
vec![
FormattedTextLine::CodeBlock(CodeBlockText {
lang: language.to_string(),
code: content.to_string(),
}),
FormattedTextLine::LineBreak,
]
}
/// The lines for a single saved cell output. Skipped outputs produce no lines.
fn output_lines(output: &Output) -> Vec<FormattedTextLine> {
match output.output_type.as_str() {
"stream" => match &output.text {
Some(text) => text_output_lines(&text.to_text()),
None => Vec::new(),
},
"execute_result" | "display_data" => {
let Some(data) = &output.data else {
return Vec::new();
};
// Prefer images, then plain text. Other MIME types (text/html,
// LaTeX, widgets, ...) are intentionally skipped in v1.
if let Some(value) = data.get("image/png") {
image_lines("image/png", value)
} else if let Some(value) = data.get("image/jpeg") {
image_lines("image/jpeg", value)
} else if let Some(value) = data.get("text/plain") {
text_output_lines(&value_to_text(value))
} else {
Vec::new()
}
}
"error" => {
let traceback = output
.traceback
.as_ref()
.map(|tb| tb.join("\n"))
.unwrap_or_default();
// ANSI escapes (common in colored tracebacks) are stripped centrally
// by `text_output_lines`.
text_output_lines(&traceback)
}
// Unknown output types are skipped.
_ => Vec::new(),
}
}
/// A text output as a plain (unhighlighted) code block. Empty output produces
/// no lines.
///
/// TODO: Bounding pathologically large outputs is left to
/// a future change that models truncation without polluting buffer content
/// (e.g. a "show more" affordance) rather than injecting placeholder text.
fn text_output_lines(text: &str) -> Vec<FormattedTextLine> {
// TODO: Remove ANSI stripping once we support ANSI-color rendering (a color
// attribute on `FormattedTextStyles` + SGR parsing)
let stripped = strip_ansi(text);
let text = stripped.trim_end_matches('\n');
if text.is_empty() {
return Vec::new();
}
code_block_lines("", text)
}
/// An embedded image output as a base64 data-URI image. Empty payloads produce
/// no lines.
fn image_lines(mime: &str, value: &serde_json::Value) -> Vec<FormattedTextLine> {
let base64: String = value_to_text(value)
.chars()
.filter(|c| !c.is_whitespace())
.collect();
if base64.is_empty() {
return Vec::new();
}
vec![
FormattedTextLine::Image(FormattedImage {
alt_text: "output".to_string(),
source: format!("data:{mime};base64,{base64}"),
title: None,
}),
FormattedTextLine::LineBreak,
]
}
/// Strip ANSI escape sequences (CSI/SGR colors, OSC, and simple escapes) so
/// tracebacks render as readable plain text.
fn strip_ansi(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '\u{1b}' {
out.push(ch);
continue;
}
match chars.peek() {
// CSI sequence: ESC [ ... <final byte 0x40-0x7E>
Some('[') => {
chars.next();
for next in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&next) {
break;
}
}
}
// OSC sequence: ESC ] ... terminated by BEL or ST (ESC \)
Some(']') => {
chars.next();
while let Some(&next) = chars.peek() {
if next == '\u{07}' {
chars.next();
break;
}
if next == '\u{1b}' {
chars.next();
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
chars.next();
}
}
// Other escapes (e.g. ESC ( B): drop the single following byte.
Some(_) => {
chars.next();
}
None => {}
}
}
out
}
/// Convert a JSON value that is either a string or an array of strings into a
/// single string (notebook source and text fields use both forms).
fn value_to_text(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(items) => items.iter().filter_map(|v| v.as_str()).collect(),
_ => String::new(),
}
}
/// Sanitize a notebook-declared language into a safe code-block language tag.
fn sanitize_language(raw: &str) -> String {
let trimmed = raw.trim();
let is_safe = !trimmed.is_empty()
&& trimmed.chars().count() <= MAX_LANGUAGE_TAG_CHARS
&& trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '#' | '-' | '_' | '.'));
if is_safe {
trimmed.to_string()
} else {
String::new()
}
}
/// Top-level notebook structure (nbformat v4, only the fields we render).
#[derive(Debug, Deserialize)]
struct Notebook {
#[serde(default)]
nbformat: Option<i64>,
cells: Vec<Cell>,
#[serde(default)]
metadata: Metadata,
}
impl Notebook {
/// The code-block language, derived from notebook metadata and sanitized to
/// a safe tag (see [`sanitize_language`]). Empty if the notebook does not
/// declare a language, or declares one that is not a safe identifier.
fn language(&self) -> String {
let raw = self
.metadata
.language_info
.as_ref()
.and_then(|info| info.name.clone())
.or_else(|| {
self.metadata
.kernelspec
.as_ref()
.and_then(|spec| spec.language.clone())
})
.unwrap_or_default();
sanitize_language(&raw)
}
}
#[derive(Debug, Default, Deserialize)]
struct Metadata {
#[serde(default)]
language_info: Option<LanguageInfo>,
#[serde(default)]
kernelspec: Option<Kernelspec>,
}
#[derive(Debug, Deserialize)]
struct LanguageInfo {
#[serde(default)]
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct Kernelspec {
#[serde(default)]
language: Option<String>,
}
#[derive(Debug, Deserialize)]
struct Cell {
#[serde(default)]
cell_type: String,
#[serde(default)]
source: Source,
#[serde(default)]
outputs: Vec<Output>,
}
#[derive(Debug, Deserialize)]
struct Output {
#[serde(default)]
output_type: String,
/// Present for `stream` outputs.
#[serde(default)]
text: Option<Source>,
/// Present for `execute_result` / `display_data` outputs (MIME -> value).
#[serde(default)]
data: Option<BTreeMap<String, serde_json::Value>>,
/// Present for `error` outputs.
#[serde(default)]
traceback: Option<Vec<String>>,
}
/// A notebook `source`/`text` field, which may be a single string or a list of
/// strings (each typically including its trailing newline).
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Source {
Lines(Vec<String>),
Text(String),
}
impl Default for Source {
fn default() -> Self {
Source::Text(String::new())
}
}
impl Source {
fn to_text(&self) -> String {
match self {
Source::Lines(lines) => lines.concat(),
Source::Text(text) => text.clone(),
}
}
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;
+413
View File
@@ -0,0 +1,413 @@
use markdown_parser::{CodeBlockText, FormattedImage, FormattedText, FormattedTextLine};
use super::*;
/// Convert a notebook, asserting it parses successfully (GFM tables off).
fn convert(json: &str) -> FormattedText {
ipynb_to_formatted_text(json, false).expect("should convert")
}
/// All code blocks in the formatted text, in order.
fn code_blocks(ft: &FormattedText) -> Vec<&CodeBlockText> {
ft.lines
.iter()
.filter_map(|line| match line {
FormattedTextLine::CodeBlock(block) => Some(block),
_ => None,
})
.collect()
}
/// All images in the formatted text, in order.
fn images(ft: &FormattedText) -> Vec<&FormattedImage> {
ft.lines
.iter()
.filter_map(|line| match line {
FormattedTextLine::Image(image) => Some(image),
_ => None,
})
.collect()
}
#[test]
fn test_markdown_and_code_cells() {
let json = r##"{
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {"language_info": {"name": "python"}},
"cells": [
{"cell_type": "markdown", "source": ["# Title\n", "Some text"]},
{"cell_type": "code", "source": "print('hi')", "outputs": []}
]
}"##;
let ft = convert(json);
// The markdown cell is parsed once into formatted text...
let raw = ft.raw_text();
assert!(raw.contains("Title"), "got: {raw:?}");
assert!(raw.contains("Some text"), "got: {raw:?}");
// ...and the code cell becomes a structured code block (no fence/re-parse).
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].lang, "python");
assert_eq!(blocks[0].code, "print('hi')");
}
#[test]
fn test_code_cell_without_language() {
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "x = 1"}
]
}"#;
let blocks_owned = convert(json);
let blocks = code_blocks(&blocks_owned);
assert_eq!(blocks.len(), 1);
// No language tag when the notebook does not declare one.
assert_eq!(blocks[0].lang, "");
assert_eq!(blocks[0].code, "x = 1");
}
#[test]
fn test_language_falls_back_to_kernelspec() {
let json = r#"{
"nbformat": 4,
"metadata": {"kernelspec": {"language": "julia"}},
"cells": [{"cell_type": "code", "source": "1 + 1"}]
}"#;
let ft = convert(json);
assert_eq!(code_blocks(&ft)[0].lang, "julia");
}
#[test]
fn test_language_with_backticks_is_sanitized() {
// A hostile language tag containing backticks (and a newline) must never end
// up as a code block's language. Because the language is now a struct field
// (not a fence), this can't break rendering regardless; we still drop it.
let json = r#"{
"nbformat": 4,
"metadata": {"language_info": {"name": "py```\ninjected"}},
"cells": [{"cell_type": "code", "source": "x = 1"}]
}"#;
let ft = convert(json);
let blocks = code_blocks(&ft);
assert_eq!(blocks[0].lang, "");
assert_eq!(blocks[0].code, "x = 1");
}
#[test]
fn test_language_with_special_chars_is_preserved() {
// Legitimate language names containing `+`/`#`/`-` are kept verbatim.
let json = r#"{
"nbformat": 4,
"metadata": {"language_info": {"name": "c++"}},
"cells": [{"cell_type": "code", "source": "int x;"}]
}"#;
let ft = convert(json);
assert_eq!(code_blocks(&ft)[0].lang, "c++");
}
#[test]
fn test_sanitize_language_accepts_and_rejects() {
// Identifier-like tokens (including the punctuation real language names use)
// are accepted and trimmed.
assert_eq!(sanitize_language("python"), "python");
assert_eq!(sanitize_language("C++"), "C++");
assert_eq!(sanitize_language("objective-c"), "objective-c");
assert_eq!(sanitize_language(" rust "), "rust");
// Backticks, whitespace, other info-string syntax, and oversized values are
// rejected, yielding an empty (safe) tag.
assert_eq!(sanitize_language("py`thon"), "");
assert_eq!(sanitize_language("two words"), "");
assert_eq!(sanitize_language(""), "");
assert_eq!(
sanitize_language(&"a".repeat(MAX_LANGUAGE_TAG_CHARS + 1)),
""
);
}
#[test]
fn test_stream_output_renders_as_text_block() {
let json = r#"{
"nbformat": 4,
"metadata": {"language_info": {"name": "python"}},
"cells": [
{"cell_type": "code", "source": "print('hello')", "outputs": [
{"output_type": "stream", "name": "stdout", "text": ["hello\n"]}
]}
]
}"#;
let ft = convert(json);
let blocks = code_blocks(&ft);
// Code cell, then its (unhighlighted) text output.
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0].lang, "python");
assert_eq!(blocks[0].code, "print('hello')");
assert_eq!(blocks[1].lang, "");
assert_eq!(blocks[1].code, "hello");
}
#[test]
fn test_execute_result_text_plain() {
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "2 + 2", "outputs": [
{"output_type": "execute_result", "data": {"text/plain": "4"}, "metadata": {}}
]}
]
}"#;
let ft = convert(json);
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[1].code, "4");
}
#[test]
fn test_error_traceback_strips_ansi() {
// Traceback lines containing SGR color escape sequences.
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "boom", "outputs": [
{"output_type": "error", "ename": "NameError", "evalue": "boom",
"traceback": ["\u001b[0;31mNameError\u001b[0m: name 'boom'", "is not defined"]}
]}
]
}"#;
let ft = convert(json);
let raw = ft.raw_text();
assert!(
!raw.contains('\u{1b}'),
"ANSI escapes should be stripped: {raw:?}"
);
assert!(raw.contains("NameError: name 'boom'"), "got: {raw:?}");
assert!(raw.contains("is not defined"), "got: {raw:?}");
}
#[test]
fn test_png_output_renders_as_data_uri_image() {
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "plot()", "outputs": [
{"output_type": "display_data", "data": {"image/png": "iVBORw0KGgo=\n"}, "metadata": {}}
]}
]
}"#;
let ft = convert(json);
let images = images(&ft);
assert_eq!(images.len(), 1);
// Whitespace in the embedded base64 is stripped.
assert_eq!(images[0].source, "data:image/png;base64,iVBORw0KGgo=");
assert_eq!(images[0].alt_text, "output");
}
#[test]
fn test_image_payload_is_not_validated_by_parser() {
// The parser no longer size-checks or validates base64 payloads: it emits
// the `data:` URI verbatim and defers decoding and size limits to the shared
// asset layer (mirroring how Markdown `data:` images are handled). An
// undecodable payload is still emitted as an image here and simply fails to
// load at render time (silent omission).
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "plot()", "outputs": [
{"output_type": "display_data", "data": {"image/png": "not-valid-base64"}, "metadata": {}}
]}
]
}"#;
let ft = convert(json);
let images = images(&ft);
assert_eq!(
images.len(),
1,
"payload should be emitted without parser-side validation"
);
assert_eq!(images[0].source, "data:image/png;base64,not-valid-base64");
}
#[test]
fn test_image_preferred_over_text_plain() {
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "plot()", "outputs": [
{"output_type": "execute_result",
"data": {"image/png": "AAAA", "text/plain": "<Figure>"}, "metadata": {}}
]}
]
}"#;
let ft = convert(json);
assert_eq!(images(&ft).len(), 1);
assert_eq!(images(&ft)[0].source, "data:image/png;base64,AAAA");
assert!(
!ft.raw_text().contains("<Figure>"),
"text/plain should be skipped when an image exists"
);
}
#[test]
fn test_unsupported_mime_is_skipped() {
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "html()", "outputs": [
{"output_type": "execute_result", "data": {"text/html": "<b>hi</b>"}, "metadata": {}}
]}
]
}"#;
let ft = convert(json);
// The code still renders; the unsupported HTML output is dropped.
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, "html()");
assert!(images(&ft).is_empty());
}
#[test]
fn test_backticks_in_code_are_stored_verbatim() {
// Source containing a triple-backtick run is stored verbatim in the code
// block; there is no fence to escape, so it cannot break out.
let json = r#"{
"nbformat": 4,
"cells": [
{"cell_type": "code", "source": "s = \"\"\"```\""}
]
}"#;
let ft = convert(json);
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, "s = \"\"\"```\"");
}
#[test]
fn test_empty_notebook_is_ok() {
let json = r#"{"nbformat": 4, "cells": []}"#;
let ft = convert(json);
assert!(
ft.lines.is_empty(),
"expected no lines, got: {:?}",
ft.lines
);
}
#[test]
fn test_malformed_json_is_error() {
let result = ipynb_to_formatted_text("{ not valid json", false);
assert!(matches!(result, Err(IpynbError::Parse(_))));
}
#[test]
fn test_non_v4_notebook_is_error() {
let json = r#"{"nbformat": 3, "cells": []}"#;
let result = ipynb_to_formatted_text(json, false);
assert!(matches!(
result,
Err(IpynbError::UnsupportedFormat { nbformat: Some(3) })
));
}
#[test]
fn test_missing_nbformat_is_error() {
// Arbitrary JSON that lacks an nbformat field must not render as a blank
// notebook; it should error so the caller falls back to raw content.
let json = r#"{"some": "json", "cells": []}"#;
let result = ipynb_to_formatted_text(json, false);
assert!(matches!(
result,
Err(IpynbError::UnsupportedFormat { nbformat: None })
));
}
#[test]
fn test_missing_cells_is_error() {
// A v4 notebook that omits the required `cells` field must error rather than
// deserialize as an empty (blank-rendering) notebook, so the caller falls
// back to raw content. An explicit `"cells": []` remains a valid empty
// notebook (see `test_empty_notebook_is_ok`).
let json = r#"{"nbformat": 4}"#;
let result = ipynb_to_formatted_text(json, false);
assert!(matches!(result, Err(IpynbError::Parse(_))));
}
#[test]
fn test_raw_cell_rendered_as_plain_block() {
let json = r#"{
"nbformat": 4,
"metadata": {"language_info": {"name": "python"}},
"cells": [
{"cell_type": "raw", "source": "raw content"}
]
}"#;
let ft = convert(json);
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
// Raw cells are not tagged with the kernel language.
assert_eq!(blocks[0].lang, "");
assert_eq!(blocks[0].code, "raw content");
}
#[test]
fn test_strip_ansi_handles_csi_and_osc() {
assert_eq!(strip_ansi("\u{1b}[0;31mred\u{1b}[0m"), "red");
assert_eq!(strip_ansi("plain text"), "plain text");
// OSC sequence terminated by BEL.
assert_eq!(strip_ansi("\u{1b}]0;title\u{07}body"), "body");
}
#[test]
fn test_large_text_output_is_preserved_verbatim() {
// Large outputs are rendered in full (no arbitrary truncation or synthetic
// placeholder text) so select-all/copy yields the canonical content.
let big = "a".repeat(250_000);
let json = format!(
r#"{{"nbformat": 4, "cells": [{{"cell_type": "code", "source": "x", "outputs": [{{"output_type": "stream", "name": "stdout", "text": "{big}"}}]}}]}}"#
);
let ft = convert(&json);
let blocks = code_blocks(&ft);
let output = &blocks[1].code;
assert!(
!output.contains("[output truncated]"),
"output must not contain a synthetic truncation marker"
);
assert_eq!(output.chars().count(), big.chars().count());
}
#[test]
fn test_raw_fallback_holds_content_verbatim() {
// Content that is not a parseable notebook is placed in a single json code
// block so any Markdown/HTML inside it is shown verbatim, not interpreted.
let raw = "{ \"nbformat\": 4, # Heading <b>bold</b>";
let ft = raw_fallback_formatted_text(raw);
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].lang, "json");
assert_eq!(blocks[0].code, raw);
}
#[test]
fn test_raw_fallback_handles_backticks_verbatim() {
// Raw content containing a triple-backtick run is stored verbatim; there is
// no fence for it to break out of.
let ft = raw_fallback_formatted_text("```");
let blocks = code_blocks(&ft);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].code, "```");
}