feat: improve native preview (#15223)

#### PR Dependency Tree


* **PR #15223** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Updated Mermaid and Typst preview rendering in native/mobile apps to
use a shared preview implementation.
* Improved production Release builds with additional Rust release tuning
and enhanced iOS Release stripping/symbol settings.
* Enabled stronger release output optimization for the native package
build.
* **Bug Fixes**
* Strengthened handling of unsupported self-hosted server versions
during login preflight.
* Refreshed unsupported-version messaging with localized text and a
direct upgrade link.
* **Tests**
* Added coverage for login preflight behavior when the server version is
unsupported.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-13 03:23:08 +08:00
committed by GitHub
parent 9b81c6debd
commit 0c16d7110d
15 changed files with 997 additions and 319 deletions
+25 -105
View File
@@ -1,10 +1,10 @@
use std::path::PathBuf;
use mermaid_rs_renderer::RenderOptions;
use affine_preview::{
MermaidRenderOptions as PreviewMermaidRenderOptions, TypstRenderOptions as PreviewTypstRenderOptions,
};
use napi::{Error, Result};
use napi_derive::napi;
use typst::layout::{Abs, PagedDocument};
use typst_as_lib::{TypstEngine, typst_kit_options::TypstKitFontOptions};
#[napi(object)]
pub struct MermaidRenderOptions {
@@ -24,31 +24,22 @@ pub struct MermaidRenderResult {
pub svg: String,
}
fn resolve_mermaid_render_options(options: Option<MermaidRenderOptions>) -> RenderOptions {
let mut render_options = match options.as_ref().and_then(|options| options.theme.as_deref()) {
Some("default") => RenderOptions::mermaid_default(),
Some("dark") | Some("modern") => RenderOptions::modern(),
_ => RenderOptions::modern(),
};
if let Some(options) = options {
if let Some(font_family) = options.font_family {
render_options.theme.font_family = font_family;
}
if let Some(font_size) = options.font_size {
render_options.theme.font_size = font_size as f32;
}
}
render_options
}
#[napi]
pub fn render_mermaid_svg(request: MermaidRenderRequest) -> Result<MermaidRenderResult> {
let render_options = resolve_mermaid_render_options(request.options);
let svg = mermaid_rs_renderer::render_with_options(&request.code, render_options)
.map_err(|error| Error::from_reason(error.to_string()))?;
let options = request.options.unwrap_or(MermaidRenderOptions {
theme: None,
font_family: None,
font_size: None,
});
let svg = affine_preview::render_mermaid_svg(
&request.code,
PreviewMermaidRenderOptions {
theme: options.theme,
font_family: options.font_family,
font_size: options.font_size,
},
)
.map_err(|error| Error::from_reason(error.to_string()))?;
Ok(MermaidRenderResult { svg })
}
@@ -106,87 +97,16 @@ fn resolve_typst_font_dirs(options: &Option<TypstRenderOptions>) -> Vec<PathBuf>
font_dirs
}
fn normalize_typst_svg(svg: String) -> String {
let mut svg = svg;
let page_background_marker = r##"<path class="typst-shape""##;
let mut cursor = 0;
while let Some(relative_idx) = svg[cursor..].find(page_background_marker) {
let idx = cursor + relative_idx;
let rest = &svg[idx..];
let Some(relative_end) = rest.find("/>") else {
break;
};
let end = idx + relative_end + 2;
let path_fragment = &svg[idx..end];
let is_page_background_path =
path_fragment.contains(r#"d="M 0 0v "#) && path_fragment.contains(r#" h "#) && path_fragment.contains(r#" v -"#);
if is_page_background_path {
svg.replace_range(idx..end, "");
cursor = idx;
continue;
}
cursor = end;
}
svg
}
#[napi]
pub fn render_typst_svg(request: TypstRenderRequest) -> Result<TypstRenderResult> {
let font_dirs = resolve_typst_font_dirs(&request.options);
let search_options = TypstKitFontOptions::new()
.include_system_fonts(false)
.include_embedded_fonts(true)
.include_dirs(font_dirs);
let engine = TypstEngine::builder()
.main_file(request.code)
.search_fonts_with(search_options)
.with_package_file_resolver()
.build();
let document = engine
.compile::<PagedDocument>()
.output
.map_err(|error| Error::from_reason(error.to_string()))?;
let svg = normalize_typst_svg(typst_svg::svg_merged(&document, Abs::pt(0.0)));
let svg = affine_preview::render_typst_svg(
&request.code,
PreviewTypstRenderOptions {
font_dirs,
cache_dir: None,
},
)
.map_err(|error| Error::from_reason(error.to_string()))?;
Ok(TypstRenderResult { svg })
}
#[cfg(test)]
mod tests {
use super::normalize_typst_svg;
#[test]
fn normalize_typst_svg_removes_all_backgrounds() {
let input = r##"<svg>
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 10 h 10 v -10 Z "/>
<g></g>
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0v 10 h 10 v -10 Z "/>
<g transform="matrix(1 0 0 1 0 10)"></g>
</svg>"##
.to_string();
let normalized = normalize_typst_svg(input);
let retained = normalized
.matches(r##"<path class="typst-shape" fill="#ffffff" fill-rule="nonzero""##)
.count();
assert_eq!(retained, 0);
}
#[test]
fn normalize_typst_svg_keeps_non_background_paths() {
let input = r##"<svg>
<path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 1 2 L 3 4 Z "/>
</svg>"##
.to_string();
let normalized = normalize_typst_svg(input);
assert!(normalized.contains(r##"d="M 1 2 L 3 4 Z ""##));
}
}