use lex_extension::wire::{Format, HostNodeKind};
use lex_extension::{
schema::{BodyKind, Schema},
AnnotationBody, LabelCtx, NodeRef, RenderOut,
};
use lex_extension_host::Registry;
use crate::ir::nodes::{Annotation, DocNode, Document, InlineContent, Verbatim};
use crate::ir::to_wire::{ir_annotation_body, ir_params_to_json};
pub struct RenderedNode {
pub label: String,
pub output: Option<String>,
pub diagnostic: Option<String>,
}
pub struct RenderPlan {
pub nodes: Vec<RenderedNode>,
pub doc_scope_count: usize,
pub root_diagnostics: Vec<String>,
}
pub fn dispatch_render(document: &Document, registry: &Registry, format_name: &str) -> RenderPlan {
let mut nodes = Vec::new();
if registry.namespace_count() == 0 {
return RenderPlan {
nodes,
doc_scope_count: 0,
root_diagnostics: Vec::new(),
};
}
let format = format_for_name(format_name);
let mut ctx = WalkCtx {
registry,
format: &format,
out: &mut nodes,
};
for ann in &document.document_annotations {
visit_annotation(ann, HostNodeKind::Document, &mut ctx);
}
let doc_scope_count = ctx.out.len();
for child in &document.children {
walk_doc_node(child, HostNodeKind::Session, &mut ctx);
}
let root_diagnostics = registry
.take_root_diagnostics()
.into_iter()
.map(|d| d.message)
.collect();
RenderPlan {
nodes,
doc_scope_count,
root_diagnostics,
}
}
fn format_for_name(name: &str) -> Format {
match name.to_ascii_lowercase().as_str() {
"html" => Format::Html,
"markdown" | "md" => Format::Markdown,
"latex" | "tex" => Format::Latex,
"pdf" => Format::Pdf,
other => Format::Custom(other.to_string()),
}
}
struct WalkCtx<'a> {
registry: &'a Registry,
format: &'a Format,
out: &'a mut Vec<RenderedNode>,
}
fn walk_doc_node(node: &DocNode, parent_kind: HostNodeKind, ctx: &mut WalkCtx<'_>) {
match node {
DocNode::Heading(h) => {
for child in &h.children {
walk_doc_node(child, HostNodeKind::Session, ctx);
}
}
DocNode::Paragraph(_) | DocNode::Inline(_) => {}
DocNode::List(l) => {
for item in &l.items {
for child in &item.children {
walk_doc_node(child, HostNodeKind::ListItem, ctx);
}
}
}
DocNode::ListItem(li) => {
for child in &li.children {
walk_doc_node(child, HostNodeKind::ListItem, ctx);
}
}
DocNode::Definition(d) => {
for child in &d.description {
walk_doc_node(child, HostNodeKind::Definition, ctx);
}
}
DocNode::Annotation(a) => {
visit_annotation(a, parent_kind, ctx);
}
DocNode::Verbatim(v) => {
visit_verbatim(v, ctx);
}
DocNode::Table(t) => {
for cell in t.header.iter().chain(t.rows.iter()).flat_map(|r| &r.cells) {
for child in &cell.content {
walk_doc_node(child, HostNodeKind::Table, ctx);
}
}
for child in &t.footnotes {
walk_doc_node(child, HostNodeKind::List, ctx);
}
}
DocNode::Image(_) | DocNode::Video(_) | DocNode::Audio(_) | DocNode::Document(_) => {}
}
}
fn visit_annotation(annotation: &Annotation, attached_to: HostNodeKind, ctx: &mut WalkCtx<'_>) {
let label = annotation.label.clone();
if let Some(schema) = ctx.registry.schema_for(&label) {
if schema_has_render(&schema, ctx.format.as_str()) {
let body = body_for_schema(&annotation.content, &schema);
let label_ctx = LabelCtx {
label: label.clone(),
params: ir_params_to_json(&annotation.parameters),
body,
node: NodeRef {
kind: attached_to.as_str().to_string(),
range: zero_range(),
origin: None,
},
};
ctx.out
.push(dispatch_one(&label, ctx.registry, &label_ctx, ctx.format));
}
}
for child in &annotation.content {
walk_doc_node(child, HostNodeKind::Annotation, ctx);
}
}
fn visit_verbatim(v: &Verbatim, ctx: &mut WalkCtx<'_>) {
let Some(label) = v.language.as_deref() else {
return;
};
if label.is_empty() {
return;
}
let Some(schema) = ctx.registry.schema_for(label) else {
return;
};
if !schema.verbatim_label || !schema_has_render(&schema, ctx.format.as_str()) {
return;
}
let params_object = serde_json::Value::Object(
v.parameters
.iter()
.map(|(k, val)| (k.clone(), serde_json::Value::String(val.clone())))
.collect(),
);
let label_ctx = LabelCtx {
label: label.to_string(),
params: params_object,
body: AnnotationBody::Text(v.content.clone()),
node: NodeRef {
kind: HostNodeKind::Verbatim.as_str().to_string(),
range: zero_range(),
origin: None,
},
};
ctx.out
.push(dispatch_one(label, ctx.registry, &label_ctx, ctx.format));
}
fn dispatch_one(label: &str, registry: &Registry, ctx: &LabelCtx, format: &Format) -> RenderedNode {
match registry.dispatch_render(ctx, format.clone()) {
Ok(Some(RenderOut::String { string })) => RenderedNode {
label: label.to_string(),
output: Some(string),
diagnostic: None,
},
Ok(Some(RenderOut::WireAst { .. })) => RenderedNode {
label: label.to_string(),
output: None,
diagnostic: Some(format!(
"handler returned WireAst output for label `{label}` but the requested format is string-shaped; falling back to default rendering"
)),
},
Ok(None) => RenderedNode {
label: label.to_string(),
output: None,
diagnostic: None,
},
Err(diag) => RenderedNode {
label: label.to_string(),
output: None,
diagnostic: Some(diag.message),
},
}
}
fn schema_has_render(schema: &Schema, format_name: &str) -> bool {
schema
.hooks
.render
.iter()
.any(|h| h.0.eq_ignore_ascii_case(format_name))
}
fn body_for_schema(content: &[DocNode], schema: &Schema) -> AnnotationBody {
if schema.body.kind == BodyKind::Text {
let flat = flatten_text_body(content);
if flat.is_empty() {
AnnotationBody::None
} else {
AnnotationBody::Text(flat)
}
} else {
ir_annotation_body(content)
}
}
fn flatten_text_body(content: &[DocNode]) -> String {
fn flatten_inlines(inlines: &[InlineContent], buf: &mut String) {
for inline in inlines {
match inline {
InlineContent::Text(t)
| InlineContent::Code(t)
| InlineContent::Math(t)
| InlineContent::Reference { raw: t, .. } => buf.push_str(t),
InlineContent::Link { text, .. } => buf.push_str(text),
InlineContent::Bold(children) | InlineContent::Italic(children) => {
flatten_inlines(children, buf);
}
InlineContent::Image(_) => {}
}
}
}
let mut buf = String::new();
let mut first = true;
for node in content {
match node {
DocNode::Paragraph(p) => {
if !first {
buf.push(' ');
}
first = false;
flatten_inlines(&p.content, &mut buf);
}
DocNode::Inline(i) => {
flatten_inlines(std::slice::from_ref(i), &mut buf);
}
_ => {}
}
}
buf
}
fn zero_range() -> lex_extension::wire::Range {
use lex_extension::wire::{Position, Range};
Range::new(Position::new(0, 0), Position::new(0, 0))
}
#[cfg(test)]
mod tests;