1
0
mirror of https://github.com/mfontanini/presenterm.git synced 2026-09-01 23:33:21 -06:00

Initial changes

This commit is contained in:
Nicholas Omann 2026-07-20 21:48:06 -05:00
parent 5f8add11a2
commit bfdfd7f506
10 changed files with 97 additions and 97 deletions

View File

@ -40,6 +40,7 @@ libc = "0.2"
vte = "0.15"
termbg = "0.6.2"
vt100 = "0.16"
merman = { version = "0.7.0", features = ["raster"] }
[dev-dependencies]
rstest = { version = "0.26", default-features = false }

View File

@ -544,27 +544,15 @@
"MermaidConfig": {
"type": "object",
"properties": {
"cli": {
"description": "The application to use to generate diagrams.",
"default": "mmdc",
"type": "string"
},
"config_path": {
"description": "A path to a mermaid JSON configuration file to be used by the `mmdc` tool.",
"type": [
"string",
"null"
]
},
"puppeteer_config_path": {
"description": "A path to a puppeteer JSON configuration file to be used by the `mmdc` tool.",
"description": "A path to a mermaid JSON configuration file applied as site config defaults.",
"type": [
"string",
"null"
]
},
"scale": {
"description": "The scaling parameter to be used in the mermaid CLI.",
"description": "The scaling parameter used when rasterizing mermaid diagrams.",
"default": 2,
"type": "integer",
"format": "uint32",

View File

@ -15,7 +15,7 @@ typst:
ppi: 300
mermaid:
# the scale parameter passed to the mermaid CLI (mmdc).
# the scale parameter used when rasterizing mermaid diagrams.
scale: 2
options:

View File

@ -292,32 +292,23 @@ snippet:
## Mermaid
The following configuration parameters can be set to alter the behavior when displaying
[mermaid](https://mermaid.js.org/) diagrams.
[mermaid](https://mermaid.js.org/) diagrams. Rendering is performed in-process with
[merman](https://github.com/Latias94/merman); no external CLI is required.
### Config file
A custom [mermaid config file](https://mermaid.ai/open-source/config/schema-docs/config.html) can be configured via the
`mermaid.config_file` config parameter. This should point to a configuration file where you can set any configs you
consider appropriate, such as the font family to use:
`mermaid.config_path` config parameter. This should point to a JSON configuration file applied as mermaid site config
defaults (for example font family). Theme values from the presentation theme still take precedence over this file:
```yaml
mermaid:
config_file: /home/foo/my_config_file.yml
```
### Puppeteer config file
A custom puppeteer config file can be configured via the `mermaid.puppeteer_config_file` config parameter. This should
point to a configuration file that will be given to puppeteer by the `mmdc` tool:
```yaml
mermaid:
puppeteer_config_file: /home/foo/puppeteer.json
config_path: /home/foo/mermaid-config.json
```
### Scaling
mermaid graphs will use a default scaling of `2` when invoking the mermaid CLI. If you'd like to change this use:
mermaid graphs will use a default raster scale of `2`. If you'd like to change this use:
```yaml

View File

@ -11,12 +11,12 @@ sequenceDiagram
```
~~~
**This requires having [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) installed**.
Mermaid diagrams are rendered in-process via the [merman](https://github.com/Latias94/merman) Rust library. No external
`mmdc` / mermaid-cli installation or browser is required.
Note that because the mermaid CLI will spin up a browser under the hood, this may not work in all environments and can
also be a bit slow (e.g. ~2 seconds to generate every image). Mermaid graphs are rendered asynchronously by a number of
threads that can be configured in the [configuration file](../../configuration/settings.md#snippet-rendering-threads).
This configuration value currently defaults to 2.
Mermaid graphs are rendered asynchronously by a number of threads that can be configured in the
[configuration file](../../configuration/settings.md#snippet-rendering-threads). This configuration value currently
defaults to 2.
The size of the rendered image can be configured by changing:
* The `mermaid.scale` [configuration parameter](../../configuration/settings.md#mermaid-scaling).
@ -41,7 +41,7 @@ cause the image to become blurry.
The theme of the rendered mermaid diagrams can be changed through the following [theme](../themes/introduction.md)
parameters:
* `mermaid.background` the background color passed to the CLI (e.g., `transparent`, `red`, `#F0F0F0`).
* `mermaid.background` the background color used when rasterizing (e.g., `transparent`, `red`, `#F0F0F0`).
* `mermaid.theme` the [mermaid theme](https://mermaid.js.org/config/theming.html#available-themes) to use.
## Always render diagrams

View File

@ -352,7 +352,7 @@ block_quote:
The [mermaid](https://mermaid.js.org/) graphs can be customized using the following parameters:
* `mermaid.background` the background color passed to the CLI (e.g., `transparent`, `red`, `#F0F0F0`).
* `mermaid.background` the background color used when rasterizing (e.g., `transparent`, `red`, `#F0F0F0`).
* `mermaid.theme` the [mermaid theme](https://mermaid.js.org/config/theming.html#available-themes) to use.
```yaml

View File

@ -343,29 +343,17 @@ pub(crate) fn default_typst_ppi() -> u32 {
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MermaidConfig {
/// The application to use to generate diagrams.
#[serde(default = "default_mermaid_cli")]
pub cli: String,
/// The scaling parameter to be used in the mermaid CLI.
/// The scaling parameter used when rasterizing mermaid diagrams.
#[serde(default = "default_mermaid_scale")]
pub scale: u32,
/// A path to a puppeteer JSON configuration file to be used by the `mmdc` tool.
pub puppeteer_config_path: Option<String>,
/// A path to a mermaid JSON configuration file to be used by the `mmdc` tool.
/// A path to a mermaid JSON configuration file applied as site config defaults.
pub config_path: Option<String>,
}
impl Default for MermaidConfig {
fn default() -> Self {
Self {
cli: default_mermaid_cli(),
scale: default_mermaid_scale(),
puppeteer_config_path: None,
config_path: None,
}
Self { scale: default_mermaid_scale(), config_path: None }
}
}
@ -373,10 +361,6 @@ pub(crate) fn default_mermaid_scale() -> u32 {
2
}
pub(crate) fn default_mermaid_cli() -> String {
if cfg!(windows) { "mmdc.cmd" } else { "mmdc" }.to_string()
}
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]

View File

@ -253,9 +253,7 @@ impl CoreComponents {
);
let third_party_config = ThirdPartyConfigs {
typst_ppi: config.typst.ppi.to_string(),
mermaid_cli: config.mermaid.cli.clone(),
mermaid_scale: config.mermaid.scale.to_string(),
mermaid_puppeteer_file: config.mermaid.puppeteer_config_path.clone(),
mermaid_scale: config.mermaid.scale,
mermaid_config_file: config.mermaid.config_path.clone(),
d2_scale: config.d2.scale.map(|s| s.to_string()).unwrap_or_else(|| "-1".to_string()),
threads: config.snippet.render.threads,

View File

@ -1,6 +1,6 @@
use crate::{
ImageRegistry,
config::{default_mermaid_cli, default_mermaid_scale, default_snippet_render_threads, default_typst_ppi},
config::{default_mermaid_scale, default_snippet_render_threads, default_typst_ppi},
markdown::{
elements::{Line, Percent, Text},
text_style::{Color, TextStyle},
@ -19,6 +19,7 @@ use crate::{
theme::{Alignment, D2Style, MermaidStyle, PresentationTheme, TypstStyle, raw::RawColor},
tools::{ExecutionError, ThirdPartyTools},
};
use merman::render::{HeadlessRenderer, raster::RasterOptions};
use std::{
collections::{HashMap, VecDeque},
fs, io, mem,
@ -30,9 +31,7 @@ use std::{
pub struct ThirdPartyConfigs {
pub typst_ppi: String,
pub mermaid_cli: String,
pub mermaid_scale: String,
pub mermaid_puppeteer_file: Option<String>,
pub mermaid_scale: u32,
pub mermaid_config_file: Option<String>,
pub d2_scale: String,
pub threads: usize,
@ -68,10 +67,8 @@ impl ThirdPartyRender {
impl Default for ThirdPartyRender {
fn default() -> Self {
let config = ThirdPartyConfigs {
mermaid_cli: default_mermaid_cli(),
typst_ppi: default_typst_ppi().to_string(),
mermaid_scale: default_mermaid_scale().to_string(),
mermaid_puppeteer_file: None,
mermaid_scale: default_mermaid_scale(),
mermaid_config_file: None,
d2_scale: "-1".to_string(),
threads: default_snippet_render_threads(),
@ -198,35 +195,36 @@ impl Worker {
if let Some(image) = self.state.lock().unwrap().cache.get(&snippet).cloned() {
return Ok(image);
}
let workdir = tempfile::Builder::default().prefix(".presenterm").tempdir()?;
let output_path = workdir.path().join("output.png");
let input_path = workdir.path().join("input.mmd");
fs::write(&input_path, input)?;
let input_path = input_path.to_string_lossy();
let output_path_str = output_path.to_string_lossy();
let mut args = vec![
"-i",
&input_path,
"-o",
&output_path_str,
"-s",
&self.shared.config.mermaid_scale,
"-t",
&style.theme,
"-b",
&style.background,
];
if let Some(path) = &self.shared.config.mermaid_puppeteer_file {
args.extend(&["-p", path]);
}
if let Some(path) = &self.shared.config.mermaid_config_file {
args.extend(&["-c", path]);
}
let site_config = self.build_mermaid_site_config(style)?;
let renderer = HeadlessRenderer::new().with_site_config(site_config).with_diagram_id("presenterm");
let raster = RasterOptions::default()
.with_scale(self.shared.config.mermaid_scale as f32)
.with_background(style.background.clone());
let png_bytes = renderer
.render_png_sync(&input, &raster)
.map_err(|error| ThirdPartyRenderError::Mermaid(error.to_string()))?
.ok_or_else(|| ThirdPartyRenderError::Mermaid("no mermaid diagram detected in snippet".into()))?;
ThirdPartyTools::mermaid(&self.shared.config.mermaid_cli, &args).run()?;
self.load_image_bytes(snippet, &png_bytes)
}
self.load_image(snippet, &output_path)
fn build_mermaid_site_config(&self, style: &MermaidStyle) -> Result<merman::MermaidConfig, ThirdPartyRenderError> {
let mut site_config = match &self.shared.config.mermaid_config_file {
Some(path) => {
let contents = fs::read_to_string(path).map_err(|error| {
ThirdPartyRenderError::Mermaid(format!("failed to read mermaid config '{path}': {error}"))
})?;
let value: serde_json::Value = serde_json::from_str(&contents).map_err(|error| {
ThirdPartyRenderError::Mermaid(format!("invalid mermaid config JSON in '{path}': {error}"))
})?;
merman::MermaidConfig::from_value(value)
}
None => merman::MermaidConfig::empty_object(),
};
// Presentation theme settings override the optional site config file.
site_config.set_value("theme", serde_json::Value::String(style.theme.clone()));
Ok(site_config)
}
pub(crate) fn render_d2(&self, input: String, style: &D2Style) -> Result<Image, ThirdPartyRenderError> {
@ -309,7 +307,11 @@ impl Worker {
fn load_image(&self, snippet: ImageSnippet, path: &Path) -> Result<Image, ThirdPartyRenderError> {
let contents = fs::read(path)?;
let image = image::load_from_memory(&contents)?;
self.load_image_bytes(snippet, &contents)
}
fn load_image_bytes(&self, snippet: ImageSnippet, contents: &[u8]) -> Result<Image, ThirdPartyRenderError> {
let image = image::load_from_memory(contents)?;
let image = self.state.lock().unwrap().image_registry.register(ImageSpec::Generated(image))?;
self.state.lock().unwrap().cache.insert(snippet, image.clone());
Ok(image)
@ -332,6 +334,9 @@ pub enum ThirdPartyRenderError {
#[error("unsupported color '{0}', only RGB is supported")]
UnsupportedColor(String),
#[error("mermaid: {0}")]
Mermaid(String),
}
#[derive(Hash, PartialEq, Eq)]
@ -431,3 +436,40 @@ impl Pollable for OperationPollable {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_worker() -> Worker {
let config = ThirdPartyConfigs {
typst_ppi: default_typst_ppi().to_string(),
mermaid_scale: default_mermaid_scale(),
mermaid_config_file: None,
d2_scale: "-1".into(),
threads: 1,
};
let shared = Arc::new(Shared { config, root_dir: ".".into(), signal: Default::default() });
let state = Arc::new(Mutex::new(RenderPoolState {
requests: Default::default(),
image_registry: Default::default(),
cache: Default::default(),
}));
Worker { state, shared }
}
#[test]
fn render_mermaid_flowchart() {
let worker = test_worker();
let style = MermaidStyle { theme: "default".into(), background: "transparent".into() };
let image = worker
.render_mermaid("flowchart TD\nA[Start] --> B[Done]".into(), &style)
.expect("mermaid render should succeed");
// Generated images are registered successfully and can be cloned from the cache.
let _ = image;
let cached = worker
.render_mermaid("flowchart TD\nA[Start] --> B[Done]".into(), &style)
.expect("cached mermaid render should succeed");
let _ = cached;
}
}

View File

@ -17,10 +17,6 @@ impl ThirdPartyTools {
Tool::new("typst", args)
}
pub(crate) fn mermaid(binary: &str, args: &[&str]) -> Tool {
Tool::new(binary, args)
}
pub(crate) fn d2(args: &[&str]) -> Tool {
Tool::new("d2", args)
}