use anyhow::Result;
use cdoc_parser::ast::visitor::AstVisitor;
use cdoc_parser::ast::{Ast, CodeBlock, Inline};
use cdoc_parser::document::{CodeOutput, Document};
use cdoc_parser::notebook::{Cell, CellCommon, CellMeta, JupyterLabMeta, Notebook, NotebookMeta};
use nanoid::nanoid;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::BufWriter;
use crate::renderers::extensions::RenderExtension;
use crate::renderers::generic::GenericRenderer;
use crate::renderers::{DocumentRenderer, RenderContext, RenderElement, RenderResult};
pub struct NotebookRendererBuilder;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct NotebookRenderer;
#[typetag::serde(name = "notebook")]
impl DocumentRenderer for NotebookRenderer {
fn render_doc(
&mut self,
ctx: &mut RenderContext,
extensions: Vec<Box<dyn RenderExtension>>,
) -> Result<Document<RenderResult>> {
let renderer = GenericRenderer::default();
for mut ext in extensions {
ext.process(ctx, renderer.clone())?;
}
let writer = NotebookWriter {
notebook_meta: ctx.notebook_output_meta.clone(),
outputs: ctx.doc.code_outputs.clone(),
code_cells: vec![],
ctx,
renderer,
};
let notebook: Notebook = writer.convert(ctx.doc.content.clone())?;
let output = serde_json::to_string_pretty(¬ebook)
.expect("Invalid notebook (this is a bug)")
.into();
Ok(Document {
content: output,
meta: ctx.doc.meta.clone(),
code_outputs: ctx.doc.code_outputs.clone(),
})
}
}
pub struct NotebookWriter<'a> {
pub notebook_meta: NotebookMeta,
pub outputs: HashMap<u64, CodeOutput>,
pub code_cells: Vec<Cell>,
pub ctx: &'a RenderContext<'a>,
pub renderer: GenericRenderer,
}
impl NotebookWriter<'_> {
fn convert(mut self, mut ast: Ast) -> Result<Notebook> {
let cell_meta = CellMeta {
jupyter: Some(JupyterLabMeta {
outputs_hidden: None,
source_hidden: Some(true),
}),
..Default::default()
};
let import = Cell::Code {
common: CellCommon {
id: "css_setup".to_string(),
metadata: cell_meta,
source: r#"import requests
from IPython.core.display import HTML
HTML(f"""
<style>
@import "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css";
</style>
""")"#
.to_string(),
},
execution_count: Some(0),
outputs: vec![],
};
self.walk_ast(&mut ast.blocks)?;
let mut buf = BufWriter::new(Vec::new());
self.renderer.render(&ast.blocks, self.ctx, &mut buf)?;
let out_str = String::from_utf8(buf.into_inner()?)?;
let md_cells = out_str.split(CODE_SPLIT);
let mut cells = vec![import];
for (idx, md) in md_cells.enumerate() {
cells.push(Cell::Markdown {
common: CellCommon {
id: nanoid!(),
metadata: Default::default(),
source: md.to_string(),
},
});
if let Some(code) = self.code_cells.get(idx) {
cells.push(code.clone());
}
}
Ok(Notebook {
metadata: self.notebook_meta,
nbformat: 4,
nbformat_minor: 5,
cells,
})
}
}
const CODE_SPLIT: &str = "--+code+--";
impl AstVisitor for NotebookWriter<'_> {
fn visit_inline(&mut self, inline: &mut Inline) -> Result<()> {
if let Inline::CodeBlock(CodeBlock {
source, attributes, ..
}) = inline
{
if attributes.contains(&"cell".into()) {
let rendered = source.to_string(
self.ctx
.doc
.meta
.code_solutions
.unwrap_or(self.ctx.parser_settings.solutions),
)?;
self.code_cells.push(Cell::Code {
common: CellCommon {
id: nanoid!(),
metadata: Default::default(),
source: rendered.trim().to_string(),
},
execution_count: Some(0),
outputs: vec![], });
*inline = Inline::Text(CODE_SPLIT.into());
}
}
Ok(())
}
}