use anyhow::{Context, Result, anyhow};
use crate::core::drawio::drawio_desktop::{DrawioDesktop, ExportArguments};
use crate::core::drawio::mxfile::{Diagram, Mxfile};
use crate::core::explorer::filesystem;
use crate::core::explorer::filesystem::FilterOptions;
use crate::core::explorer::git_repository;
use relative_path::RelativePath;
use std::ffi::OsStr;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
pub struct ExporterOptions<'a> {
pub application: &'a String,
pub drawio_desktop_headless: bool,
pub folder: &'a String,
pub on_filesystem_changes: bool,
pub on_git_changes_since_reference: Option<&'a String>,
pub remove_page_suffix: bool,
pub path: &'a str,
pub format: &'a String,
pub border: &'a String,
pub scale: Option<&'a String>,
pub enable_plugins: bool,
pub width: Option<&'a String>,
pub height: Option<&'a String>,
pub crop: bool,
pub all_pages: bool,
pub transparent: bool,
pub quality: &'a String,
pub uncompressed: bool,
pub embed_svg_fonts: bool,
pub embed_svg_images: bool,
pub svg_theme: Option<&'a String>,
pub svg_links_target: Option<&'a String>,
pub embed_diagram: bool,
}
fn sanitize_diagram_name(name: &str) -> String {
name.chars()
.map(|x| match x {
' ' => '-',
'&' => '-',
'#' => '-',
'%' => '-',
'{' => '-',
'}' => '-',
'\\' => '-',
'/' => '-',
'<' => '-',
'>' => '-',
'*' => '-',
'?' => '-',
'$' => '-',
'!' => '-',
'\'' => '-',
'"' => '-',
':' => '-',
';' => '-',
',' => '-',
'@' => '-',
'+' => '-',
'`' => '-',
'|' => '-',
'=' => '-',
_ => x,
})
.collect::<String>()
}
fn resolve_export_format(format: &str) -> &str {
match format {
"adoc" | "md" => "png",
_ => format,
}
}
fn should_include_page_suffix(remove_page_suffix: bool, diagram_count: usize) -> bool {
!(remove_page_suffix && diagram_count == 1)
}
fn build_output_path(base_path: &Path, folder: &str, filename: &str) -> PathBuf {
base_path.parent().unwrap().join(folder).join(filename)
}
fn build_export_arguments<'a>(
options: &'a ExporterOptions,
input: &'a str,
output: Option<&'a str>,
format: &'a str,
page_index: Option<&'a String>,
) -> ExportArguments<'a> {
ExportArguments {
recursive: false,
output,
input,
format,
border: options.border,
scale: options.scale,
width: options.width,
height: options.height,
crop: options.crop,
embed_diagram: options.embed_diagram,
transparent: options.transparent,
quality: options.quality,
uncompressed: options.uncompressed,
all_pages: options.all_pages,
page_index,
page_range: None,
embed_svg_fonts: options.embed_svg_fonts,
embed_svg_images: options.embed_svg_images,
svg_theme: options.svg_theme,
svg_links_target: options.svg_links_target,
enable_plugins: options.enable_plugins,
}
}
pub fn exporter(options: ExporterOptions<'_>) -> Result<()> {
let input_path = match options.path {
"" => PathBuf::from("."),
path => PathBuf::from(path),
};
if !input_path.exists() {
return Err(anyhow!(format!(
"path '{}' must exist (as directory or file)",
options.path
)));
}
let drawio_files = match options.on_git_changes_since_reference {
None => {
let filter_options = match options.on_filesystem_changes {
true => FilterOptions::filter_on(options.folder),
false => FilterOptions::no_filtering(),
};
filesystem::explore_path(&input_path, filter_options)
}
Some(git_reference) => git_repository::explore_path(&input_path, git_reference),
}
.with_context(|| format!("can't explore path {}", &input_path.display()))?;
let drawio_desktop = DrawioDesktop::new(options.application, options.drawio_desktop_headless)?;
prepare_export_folders(options.folder, &drawio_files)
.with_context(|| format!("can't prepare export folders named {}", options.folder))?;
let drawio_path_base = RelativePath::new(options.path);
for (path, mxfile) in drawio_files {
let drawio_file_path = drawio_path_base.relative(RelativePath::new(path.to_str().unwrap()));
println!("+ export file : {}", drawio_file_path);
if is_pdf_all_pages_enabled(&options) || is_xml_format_enabled(&options) {
export_pdf_all_pages(&options, &drawio_desktop, &path)?;
} else {
export_per_page(&options, &drawio_desktop, &path, mxfile)?;
}
}
Ok(())
}
fn is_pdf_all_pages_enabled(options: &ExporterOptions) -> bool {
options.all_pages && options.format == "pdf"
}
fn is_xml_format_enabled(options: &ExporterOptions) -> bool {
options.format == "xml"
}
fn export_per_page(
options: &ExporterOptions,
drawio_desktop: &DrawioDesktop,
path: &Path,
mxfile: Mxfile,
) -> Result<()> {
let with_page_suffix =
should_include_page_suffix(options.remove_page_suffix, mxfile.diagrams.len());
for (position, diagram) in mxfile.diagrams.iter().enumerate() {
let position_to_use = position + 1;
let valid_diagram_name = sanitize_diagram_name(&diagram.name);
println!("- export page {} : {}", position_to_use, valid_diagram_name);
let file_stem = path.file_stem().unwrap();
let file_stem_suffix = match with_page_suffix {
true => format!("-{}", valid_diagram_name),
false => "".to_string(),
};
let real_format = resolve_export_format(options.format.as_str());
let output_filename = format!(
"{}{}.{}",
file_stem.to_str().unwrap(),
file_stem_suffix,
real_format
);
let output_path = build_output_path(path, options.folder, &output_filename);
println!("\\ generate {} file", real_format);
let page_index_str = position_to_use.to_string();
drawio_desktop.execute(build_export_arguments(
options,
path.to_str().unwrap(),
output_path.to_str(),
real_format,
Some(&page_index_str),
))?;
if options.format.eq("adoc") || options.format.eq("md") {
generate_formatted_text_file(
options,
path,
diagram,
file_stem,
file_stem_suffix,
output_filename,
)?;
}
}
Ok(())
}
fn export_pdf_all_pages(
options: &ExporterOptions,
drawio_desktop: &DrawioDesktop,
path: &Path,
) -> Result<()> {
println!("- export all pages");
println!("\\ generate {} file", options.format.as_str());
let file_stem = path.file_stem().unwrap();
let output_filename = format!(
"{}.{}",
file_stem.to_str().unwrap(),
options.format.as_str()
);
let output_path = build_output_path(path, options.folder, &output_filename);
drawio_desktop.execute(build_export_arguments(
options,
path.to_str().unwrap(),
output_path.to_str(),
options.format.as_str(),
None,
))
}
fn generate_formatted_text_file(
options: &ExporterOptions<'_>,
path: &Path,
diagram: &Diagram,
file_stem: &OsStr,
file_stem_suffix: String,
output_filename: String,
) -> Result<()> {
println!("\\ generate {} file", options.format);
let formatted_text_filename = format!(
"{}{}.{}",
file_stem.to_str().unwrap(),
file_stem_suffix,
options.format
);
let formatted_text_path = build_output_path(path, options.folder, &formatted_text_filename);
let mut file = File::create(formatted_text_path)?;
if options.format.eq("adoc") {
write!(
file,
"= {} {}
image::{}[{}]
",
file_stem.to_str().unwrap(),
diagram.name,
output_filename,
diagram.name
)?;
} else if options.format.eq("md") {
write!(
file,
"# {} {}
![{}][{}]
",
file_stem.to_str().unwrap(),
diagram.name,
diagram.name,
output_filename,
)?;
}
println!("\\ include links in {} file", options.format);
for (link, label) in diagram.get_links() {
if label.is_empty() {
println!(
"warn: link not included, due to missing label: link '[missing]' to {}",
link
);
continue;
}
if link.is_empty() {
println!(
"warn: link not included, due to missing url: link '{}' to [missing]",
label
);
continue;
}
if link.starts_with("data:page/id") {
println!(
"warn: link not included, page link isn't supported, link '{}' to {}",
label, link
);
continue;
}
println!("link '{}' to {}", label, link);
if options.format.eq("adoc") {
writeln!(file, "* {}[{}]", link.replace("--", "\\--"), label)?;
} else if options.format.eq("md") {
writeln!(file, "* [{}]({})", label, link)?;
}
}
Ok(())
}
fn prepare_export_folders(folder: &str, drawio_files: &[(PathBuf, Mxfile)]) -> Result<()> {
let parent_paths: Vec<PathBuf> = drawio_files
.iter()
.map(|(path, _)| path.parent().unwrap().to_path_buf())
.collect();
for parent_path in parent_paths {
fs::create_dir_all(parent_path.join(folder)).with_context(|| {
format!(
"can't prepare export folder named {} in path {}",
folder,
parent_path.display()
)
})?;
}
Ok(())
}