mirror of
https://github.com/mfontanini/presenterm.git
synced 2026-09-01 23:33:21 -06:00
Initial changes
This commit is contained in:
parent
5f8add11a2
commit
bfdfd7f506
@ -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 }
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user