refactor: restructure project into a workspace by introducing a plugin API crate and modularizing individual plugin definitions.
Release / check-release (push) Successful in 17s
Release / build (push) Skipped
Release / package (deb) (push) Skipped
Release / package (rpm) (push) Skipped
Release / publish (push) Skipped
CI / test (push) Failing after 2m6s

This commit is contained in:
Elias Wendland
2026-07-17 15:50:50 +02:00
parent 2ea317e117
commit eb76ed1937
43 changed files with 4243 additions and 2296 deletions
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "convertis-html"
build = "../../plugin-build.rs"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[lib]
crate-type = ["cdylib"]
[dependencies]
base64.workspace = true
convertis-plugin-api.workspace = true
[dev-dependencies]
tempfile.workspace = true
+178
View File
@@ -0,0 +1,178 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use base64::{Engine, engine::general_purpose::STANDARD};
use convertis_plugin_api::{Conversion, ConversionRequest, Plugin, PluginMetadata};
const INPUTS: &[&str] = &[
"png",
"jpeg",
"gif",
"animated-gif",
"webp",
"animated-webp",
"apng",
"bmp",
"tiff",
"ico",
"avif",
"heic",
"jxl",
"svg",
"pdf",
"mp4",
"webm",
"mkv",
"avi",
"mov",
"mpeg",
"ogv",
"mp3",
"ogg",
"aac",
"m4a",
"opus",
"wma",
"wav",
"flac",
"aiff",
"au",
"text",
];
struct Html;
fn escape(value: &str) -> String {
value
.chars()
.map(|character| match character {
'&' => "&amp;".to_owned(),
'<' => "&lt;".to_owned(),
'>' => "&gt;".to_owned(),
'"' => "&quot;".to_owned(),
'\'' => "&#39;".to_owned(),
other => other.to_string(),
})
.collect()
}
fn mime(format: &str) -> &'static str {
match format {
"png" | "apng" => "image/png",
"jpeg" => "image/jpeg",
"gif" | "animated-gif" => "image/gif",
"webp" | "animated-webp" => "image/webp",
"bmp" => "image/bmp",
"tiff" => "image/tiff",
"ico" => "image/x-icon",
"avif" => "image/avif",
"heic" => "image/heic",
"jxl" => "image/jxl",
"svg" => "image/svg+xml",
"mp4" => "video/mp4",
"webm" => "video/webm",
"ogv" => "video/ogg",
"mp3" => "audio/mpeg",
"ogg" => "audio/ogg",
"wav" => "audio/wav",
"flac" => "audio/flac",
"pdf" => "application/pdf",
_ => "application/octet-stream",
}
}
impl Plugin for Html {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
id: "html".into(),
package: "convertis-html".into(),
description: "Create self-contained HTML".into(),
conversions: INPUTS
.iter()
.map(|from| Conversion::file(from, "html", (255, 255, 255)))
.collect(),
options: vec![],
}
}
fn convert(&self, request: &ConversionRequest) -> Result<(), String> {
let body = if request.from == "text" {
let text =
std::fs::read_to_string(&request.input).map_err(|error| error.to_string())?;
format!("<p style=\"white-space:pre-wrap\">{}</p>", escape(&text))
} else {
let data =
STANDARD.encode(std::fs::read(&request.input).map_err(|error| error.to_string())?);
let uri = format!("data:{};base64,{}", mime(&request.from), data);
if matches!(
request.from.as_str(),
"png"
| "apng"
| "jpeg"
| "gif"
| "animated-gif"
| "webp"
| "animated-webp"
| "bmp"
| "tiff"
| "ico"
| "avif"
| "heic"
| "jxl"
| "svg"
) {
format!("<img alt=\"Embedded media\" src=\"{uri}\">")
} else if matches!(
request.from.as_str(),
"mp3" | "ogg" | "aac" | "m4a" | "opus" | "wma" | "wav" | "flac" | "aiff" | "au"
) {
format!("<audio controls src=\"{uri}\"></audio>")
} else if request.from == "pdf" {
format!("<embed type=\"application/pdf\" src=\"{uri}\">")
} else {
format!("<video controls src=\"{uri}\"></video>")
}
};
let document = format!(
"<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Converted media</title></head><body>{body}</body></html>\n"
);
std::fs::write(&request.output, document).map_err(|error| error.to_string())
}
}
convertis_plugin_api::export_plugin!(Html, "html");
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn text_is_escaped_inside_a_paragraph() {
let directory = tempfile::tempdir().unwrap();
let input = directory.path().join("input.txt");
let output = directory.path().join("output.html");
std::fs::write(&input, "<script>alert('x')</script>").unwrap();
Html.convert(&ConversionRequest {
input,
output: output.clone(),
from: "text".into(),
to: "html".into(),
options: BTreeMap::new(),
})
.unwrap();
let html = std::fs::read_to_string(output).unwrap();
assert!(html.contains("&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;"));
assert!(!html.contains("<script>"));
}
}