1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5
6use alloc::boxed::Box;
7use alloc::vec::Vec;
8use rushdown::as_extension_data;
9use rushdown::as_extension_data_mut;
10use rushdown::as_kind_data;
11use rushdown::as_type_data;
12use rushdown::as_type_data_mut;
13use rushdown::ast::walk;
14use rushdown::context::BoolValue;
15use rushdown::context::ContextKey;
16use rushdown::context::ContextKeyRegistry;
17use rushdown::parser::AnyAstTransformer;
18use rushdown::parser::AstTransformer;
19use rushdown::parser::ParserOptions;
20use rushdown::renderer::PostRender;
21use rushdown::renderer::Render;
22
23use core::any::TypeId;
24use core::error::Error as CoreError;
25use core::fmt;
26use core::fmt::Write;
27use core::result::Result as CoreResult;
28use std::cell::RefCell;
29use std::io::Write as _;
30use std::process::Command;
31use std::process::Stdio;
32use std::rc::Rc;
33
34use rushdown::{
35 ast::{pp_indent, Arena, KindData, NodeKind, NodeRef, NodeType, PrettyPrint, WalkStatus},
36 matches_kind,
37 parser::{self, Parser, ParserExtension, ParserExtensionFn},
38 renderer::{
39 self,
40 html::{self, Renderer, RendererExtension, RendererExtensionFn},
41 BoxRenderNode, NodeRenderer, NodeRendererRegistry, RenderNode, RendererOptions, TextWrite,
42 },
43 text::{self, Reader},
44 Result,
45};
46
47#[derive(Debug)]
51pub struct Diagram {
52 diagram_type: DiagramType,
53 value: text::Lines,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum DiagramType {
59 #[default]
60 Mermaid,
61 PlantUml,
62}
63
64impl Diagram {
65 pub fn new(diagram_type: DiagramType) -> Self {
67 Self {
68 diagram_type,
69 value: text::Lines::default(),
70 }
71 }
72
73 #[inline(always)]
75 pub fn diagram_type(&self) -> DiagramType {
76 self.diagram_type
77 }
78
79 #[inline(always)]
81 pub fn value(&self) -> &text::Lines {
82 &self.value
83 }
84
85 pub fn set_value(&mut self, value: impl Into<text::Lines>) {
87 self.value = value.into();
88 }
89}
90
91impl NodeKind for Diagram {
92 fn typ(&self) -> NodeType {
93 NodeType::LeafBlock
94 }
95
96 fn kind_name(&self) -> &'static str {
97 "Diagram"
98 }
99}
100
101impl PrettyPrint for Diagram {
102 fn pretty_print(&self, w: &mut dyn Write, source: &str, level: usize) -> fmt::Result {
103 writeln!(
104 w,
105 "{}DiagramType: {:?}",
106 pp_indent(level),
107 self.diagram_type()
108 )?;
109 write!(w, "{}Value: ", pp_indent(level))?;
110 writeln!(w, "[ ")?;
111 for line in self.value.iter(source) {
112 write!(w, "{}{}", pp_indent(level + 1), line)?;
113 }
114 writeln!(w)?;
115 writeln!(w, "{}]", pp_indent(level))
116 }
117}
118
119impl From<Diagram> for KindData {
120 fn from(e: Diagram) -> Self {
121 KindData::Extension(Box::new(e))
122 }
123}
124
125#[derive(Debug, Clone, Default)]
131pub struct DiagramParserOptions {
132 pub mermaid: MermaidParserOptions,
133 pub plantuml: PlantUmlParserOptions,
134}
135
136#[derive(Debug, Clone)]
138pub struct MermaidParserOptions {
139 pub enabled: bool,
140}
141
142impl ParserOptions for DiagramParserOptions {}
143
144impl Default for MermaidParserOptions {
145 fn default() -> Self {
146 Self { enabled: true }
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct PlantUmlParserOptions {
153 pub enabled: bool,
154}
155
156impl Default for PlantUmlParserOptions {
157 fn default() -> Self {
158 Self { enabled: true }
159 }
160}
161
162#[derive(Debug)]
163struct DiagramAstTransformer {
164 options: DiagramParserOptions,
165}
166
167impl DiagramAstTransformer {
168 pub fn with_options(options: DiagramParserOptions) -> Self {
169 Self { options }
170 }
171}
172
173impl AstTransformer for DiagramAstTransformer {
174 fn transform(
175 &self,
176 arena: &mut Arena,
177 doc_ref: NodeRef,
178 reader: &mut text::BasicReader,
179 _ctx: &mut parser::Context,
180 ) {
181 let mut target_codes: Option<Vec<NodeRef>> = None;
182 walk(arena, doc_ref, &mut |arena: &Arena,
183 node_ref: NodeRef,
184 entering: bool|
185 -> Result<WalkStatus> {
186 if entering && matches_kind!(arena[node_ref], CodeBlock) {
187 let code_block = as_kind_data!(arena[node_ref], CodeBlock);
188 if let Some(lang) = code_block.language_str(reader.source()) {
189 if lang == "mermaid" || lang == "plantuml" {
190 if target_codes.is_none() {
191 target_codes = Some(Vec::new());
192 }
193 target_codes.as_mut().unwrap().push(node_ref);
194 }
195 }
196 }
197 Ok(WalkStatus::Continue)
198 })
199 .ok();
200 if let Some(target_codes) = target_codes {
201 for code_ref in target_codes {
202 let code_block = as_kind_data!(arena[code_ref], CodeBlock);
203 let lines = code_block.value().clone();
204 let pos = arena[code_ref].pos();
205 let diagram_type = match code_block.language_str(reader.source()) {
206 Some("mermaid") => {
207 if self.options.mermaid.enabled {
208 DiagramType::Mermaid
209 } else {
210 continue;
211 }
212 }
213 Some("plantuml") => {
214 if self.options.plantuml.enabled {
215 DiagramType::PlantUml
216 } else {
217 continue;
218 }
219 }
220 _ => continue,
221 };
222 let diagram = arena.new_node(Diagram::new(diagram_type));
223 if let Some(pos) = pos {
224 arena[diagram].set_pos(pos);
225 }
226 as_extension_data_mut!(arena, diagram, Diagram).set_value(lines);
227 let hbl = as_type_data!(arena, code_ref, Block).has_blank_previous_line();
228 as_type_data_mut!(arena, diagram, Block).set_blank_previous_line(hbl);
229 arena[code_ref]
230 .parent()
231 .unwrap()
232 .replace_child(arena, code_ref, diagram);
233 }
234 }
235 }
236}
237
238impl From<DiagramAstTransformer> for AnyAstTransformer {
239 fn from(t: DiagramAstTransformer) -> Self {
240 AnyAstTransformer::Extension(Box::new(t))
241 }
242}
243
244const HAS_MERMAID_DIAGRAM: &str = "rushdown-diagram-hmd";
249
250#[derive(Debug, Clone, Default)]
252pub struct DiagramHtmlRendererOptions {
253 pub mermaid: MermaidHtmlRenderingOptions,
254 pub plantuml: PlantUmlHtmlRenderingOptions,
255}
256
257#[derive(Debug, Clone)]
259pub enum MermaidHtmlRenderingOptions {
260 Client(ClientSideMermaidHtmlRendereringOptions),
262}
263
264impl Default for MermaidHtmlRenderingOptions {
265 fn default() -> Self {
266 Self::Client(ClientSideMermaidHtmlRendereringOptions::default())
267 }
268}
269
270#[derive(Debug, Clone)]
271pub struct ClientSideMermaidHtmlRendereringOptions {
272 pub mermaid_url: &'static str,
274}
275
276impl Default for ClientSideMermaidHtmlRendereringOptions {
277 fn default() -> Self {
278 Self {
279 mermaid_url: "https://cdn.jsdelivr.net/npm/mermaid@latest/dist/mermaid.esm.min.mjs",
280 }
281 }
282}
283
284#[derive(Debug, Clone, Default)]
286pub struct PlantUmlHtmlRenderingOptions {
287 pub command: String,
290}
291
292impl RendererOptions for DiagramHtmlRendererOptions {}
293
294struct DiagramHtmlRenderer<W: TextWrite> {
295 _phantom: core::marker::PhantomData<W>,
296 options: DiagramHtmlRendererOptions,
297 writer: html::Writer,
298 has_mermaid_diagram: ContextKey<BoolValue>,
299}
300
301impl<W: TextWrite> DiagramHtmlRenderer<W> {
302 fn new(
303 html_opts: html::Options,
304 options: DiagramHtmlRendererOptions,
305 reg: Rc<RefCell<ContextKeyRegistry>>,
306 ) -> Self {
307 let has_mermaid_diagram = reg
308 .borrow_mut()
309 .get_or_create::<BoolValue>(HAS_MERMAID_DIAGRAM);
310 Self {
311 _phantom: core::marker::PhantomData,
312 options,
313 writer: html::Writer::with_options(html_opts),
314 has_mermaid_diagram,
315 }
316 }
317}
318
319impl<W: TextWrite> RenderNode<W> for DiagramHtmlRenderer<W> {
320 fn render_node<'a>(
321 &self,
322 w: &mut W,
323 source: &'a str,
324 arena: &'a Arena,
325 node_ref: NodeRef,
326 entering: bool,
327 ctx: &mut renderer::Context,
328 ) -> Result<WalkStatus> {
329 let kd = as_extension_data!(arena, node_ref, Diagram);
330 match kd.diagram_type {
331 DiagramType::Mermaid => {
332 ctx.insert(self.has_mermaid_diagram, true);
333 if matches!(self.options.mermaid, MermaidHtmlRenderingOptions::Client(_)) {
334 if entering {
335 self.writer.write_safe_str(w, "<pre class=\"mermaid\">\n")?;
336 for line in kd.value().iter(source) {
337 self.writer.raw_write(w, &line)?;
338 }
339 } else {
340 self.writer.write_safe_str(w, "</pre>\n")?;
341 }
342 }
343 }
344 DiagramType::PlantUml => {
345 if entering {
346 let mut buf = String::new();
347 for line in kd.value().iter(source) {
348 buf.push_str(&line);
349 }
350 match plant_uml(&self.options.plantuml.command, buf.as_bytes(), &[]) {
351 Ok(svg) => {
352 self.writer.write_html(w, &String::from_utf8_lossy(&svg))?;
353 }
354 Err(e) => {
355 self.writer.write_html(
356 w,
357 &format!(
358 "<pre class=\"plantuml-error\">Error rendering PlantUML diagram: {}</pre>",
359 e
360 ),
361 )?;
362 }
363 }
364 }
365 }
366 }
367 Ok(WalkStatus::Continue)
368 }
369}
370
371struct DiagramPostRenderHook<W: TextWrite> {
372 _phantom: core::marker::PhantomData<W>,
373 writer: html::Writer,
374 options: DiagramHtmlRendererOptions,
375
376 has_mermaid_diagram: ContextKey<BoolValue>,
377}
378
379impl<W: TextWrite> DiagramPostRenderHook<W> {
380 pub fn new(
381 html_opts: html::Options,
382 options: DiagramHtmlRendererOptions,
383 reg: Rc<RefCell<ContextKeyRegistry>>,
384 ) -> Self {
385 let has_mermaid_diagram = reg
386 .borrow_mut()
387 .get_or_create::<BoolValue>(HAS_MERMAID_DIAGRAM);
388
389 Self {
390 _phantom: core::marker::PhantomData,
391 writer: html::Writer::with_options(html_opts.clone()),
392 options,
393 has_mermaid_diagram,
394 }
395 }
396}
397
398impl<W: TextWrite> PostRender<W> for DiagramPostRenderHook<W> {
399 fn post_render(
400 &self,
401 w: &mut W,
402 _source: &str,
403 _arena: &Arena,
404 _node_ref: NodeRef,
405 _render: &dyn Render<W>,
406 ctx: &mut renderer::Context,
407 ) -> Result<()> {
408 if *ctx.get(self.has_mermaid_diagram).unwrap_or(&false) {
409 #[allow(irrefutable_let_patterns)]
410 if let MermaidHtmlRenderingOptions::Client(client_opts) = &self.options.mermaid {
411 self.writer.write_html(
412 w,
413 &format!(
414 r#"<script type="module">
415import mermaid from '{}';
416</script>
417"#,
418 client_opts.mermaid_url
419 ),
420 )?;
421 }
422 }
423 Ok(())
424 }
425}
426
427impl<'cb, W> NodeRenderer<'cb, W> for DiagramHtmlRenderer<W>
428where
429 W: TextWrite + 'cb,
430{
431 fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
432 nrr.register_node_renderer_fn(TypeId::of::<Diagram>(), BoxRenderNode::new(self));
433 }
434}
435
436pub fn diagram_parser_extension(options: impl Into<DiagramParserOptions>) -> impl ParserExtension {
442 ParserExtensionFn::new(|p: &mut Parser| {
443 p.add_ast_transformer(DiagramAstTransformer::with_options, options.into(), 100);
444 })
445}
446
447pub fn diagram_html_renderer_extension<'cb, W>(
449 options: impl Into<DiagramHtmlRendererOptions>,
450) -> impl RendererExtension<'cb, W>
451where
452 W: TextWrite + 'cb,
453{
454 RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
455 let options = options.into();
456 r.add_post_render_hook(DiagramPostRenderHook::new, options.clone(), 500);
457 r.add_node_renderer(DiagramHtmlRenderer::new, options);
458 })
459}
460
461fn plant_uml(
465 command: impl AsRef<str>,
466 src: &[u8],
467 args: &[&str],
468) -> CoreResult<Vec<u8>, Box<dyn CoreError>> {
469 let path = if command.as_ref().is_empty() {
470 which::which("plantuml")
471 } else {
472 Ok(std::path::PathBuf::from(command.as_ref()))
473 }?;
474
475 let mut params = vec!["-tsvg", "-p", "-Djava.awt.headless=true"];
476 params.extend_from_slice(args);
477
478 let mut cmd = Command::new(path);
479 cmd.args(¶ms)
480 .env("JAVA_OPTS", "-Djava.awt.headless=true")
481 .stdin(Stdio::piped())
482 .stdout(Stdio::piped())
483 .stderr(Stdio::piped());
484
485 let mut child = cmd.spawn()?;
486 {
487 let stdin = child.stdin.as_mut().ok_or("Failed to open stdin")?;
488 stdin.write_all(src)?;
489 }
490
491 let output = child.wait_with_output()?;
492
493 if output.status.success() {
494 Ok(output.stdout)
495 } else {
496 Err(String::from_utf8_lossy(&output.stderr).into())
497 }
498}
499