use serde::Deserialize;
use serde_json::json;
use wasm_bindgen::prelude::*;
use crate::components::{Component, WordSearch};
use crate::engine::{Engine, EngineConfig};
use crate::render::RenderRequest;
fn wasm_engine() -> Result<Engine, JsValue> {
let config = EngineConfig {
pack_paths: vec![],
font_paths: vec![],
use_embedded_fonts: true,
use_system_fonts: false,
cache_dir: None,
};
Engine::with_config(config).map_err(|e| JsValue::from_str(&format!("engine init failed: {e}")))
}
#[wasm_bindgen]
pub fn render(request_json: &str) -> Result<Vec<u8>, JsValue> {
let request: RenderRequest = serde_json::from_str(request_json)
.map_err(|e| JsValue::from_str(&format!("invalid RenderRequest JSON: {e}")))?;
wasm_engine()?
.render_pdf(&request)
.map_err(|e| JsValue::from_str(&format!("render failed: {e}")))
}
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
struct WordSearchRequest {
title: String,
#[serde(default)]
description: Option<String>,
words: Vec<String>,
#[serde(default)]
translations: Vec<Option<String>>,
#[serde(default = "default_size")]
width: usize,
#[serde(default = "default_size")]
height: usize,
#[serde(default = "default_true")]
allow_diagonal: bool,
#[serde(default)]
allow_reverse: bool,
#[serde(default = "default_seed")]
seed: u64,
#[serde(default = "default_columns")]
columns_word_list: usize,
#[serde(default = "default_language")]
language: String,
}
fn default_size() -> usize {
12
}
fn default_true() -> bool {
true
}
fn default_seed() -> u64 {
42
}
fn default_columns() -> usize {
3
}
fn default_language() -> String {
"de".into()
}
struct Copy {
explanation: &'static str,
about_title: &'static str,
about_content: &'static str,
solution_prefix: &'static str,
solution_description: &'static str,
default_description: &'static str,
footer_prefix: &'static str,
footer_tagline: &'static str,
}
fn copy_for(language: &str) -> Copy {
match language {
"en" => Copy {
explanation: "How it works: find all the listed words in the letter grid — they can be hidden horizontally, vertically, or diagonally. The solution with highlighted words is on the last page.",
about_title: "About this PDF",
about_content: "This puzzle was generated with renderreport — a PDF engine written in Rust with Typst as its rendering core (https://renderreport.casoon.de).",
solution_prefix: "Solution: ",
solution_description: "All hidden words are highlighted.",
default_description: "Find all the hidden words in the letter grid!",
footer_prefix: "Created with",
footer_tagline: "Word Search Generator",
},
"fr" => Copy {
explanation: "Comment jouer : trouvez tous les mots dans la grille de lettres – ils peuvent être cachés horizontalement, verticalement ou en diagonale. La solution avec les mots surlignés se trouve sur la dernière page.",
about_title: "À propos de ce PDF",
about_content: "Ce puzzle a été généré avec renderreport – un moteur PDF écrit en Rust avec Typst comme noyau de rendu (https://renderreport.casoon.de).",
solution_prefix: "Solution : ",
solution_description: "Tous les mots cachés sont surlignés.",
default_description: "Trouvez tous les mots cachés dans la grille de lettres !",
footer_prefix: "Créé avec",
footer_tagline: "Générateur de mots cachés",
},
"es" => Copy {
explanation: "Cómo funciona: encuentra todas las palabras en la cuadrícula de letras – pueden estar ocultas en horizontal, vertical o diagonal. La solución con las palabras resaltadas está en la última página.",
about_title: "Sobre este PDF",
about_content: "Este rompecabezas fue generado con renderreport – un motor de PDF escrito en Rust con Typst como núcleo de renderizado (https://renderreport.casoon.de).",
solution_prefix: "Solución: ",
solution_description: "Todas las palabras ocultas están resaltadas.",
default_description: "¡Encuentra todas las palabras ocultas en la cuadrícula de letras!",
footer_prefix: "Creado con",
footer_tagline: "Generador de sopa de letras",
},
_ => Copy {
explanation: "So funktioniert's: Finde alle gesuchten Wörter im Buchstabengitter – sie können waagerecht, senkrecht oder diagonal versteckt sein. Die Lösung mit farbig hervorgehobenen Wörtern findest du auf der letzten Seite.",
about_title: "Über dieses PDF",
about_content: "Dieses Rätsel wurde mit renderreport erzeugt – einer in Rust geschriebenen PDF-Engine mit Typst als Rendering-Kern (https://renderreport.casoon.de).",
solution_prefix: "Lösung: ",
solution_description: "Alle versteckten Wörter sind hervorgehoben.",
default_description: "Finde alle versteckten Wörter im Buchstabengitter!",
footer_prefix: "Erstellt mit",
footer_tagline: "Wortsuchrätsel-Generator",
},
}
}
#[wasm_bindgen]
pub fn render_wordsearch(request_json: &str) -> Result<Vec<u8>, JsValue> {
let req: WordSearchRequest = serde_json::from_str(request_json)
.map_err(|e| JsValue::from_str(&format!("invalid word search request JSON: {e}")))?;
let copy = copy_for(&req.language);
let puzzle = WordSearch::builder()
.title(req.title.clone())
.description(
req.description
.clone()
.unwrap_or_else(|| copy.default_description.into()),
)
.words(req.words.clone())
.translations(req.translations.clone())
.language(req.language.clone())
.grid_size(req.width, req.height)
.allow_diagonal(req.allow_diagonal)
.allow_reverse(req.allow_reverse)
.seed(req.seed)
.show_solution(false)
.columns_word_list(req.columns_word_list)
.build()
.map_err(|e| JsValue::from_str(&format!("invalid puzzle parameters: {e}")))?;
let solution = WordSearch::builder()
.title(format!("{}{}", copy.solution_prefix, req.title))
.description(copy.solution_description)
.words(req.words)
.translations(req.translations)
.language(req.language.clone())
.grid_size(req.width, req.height)
.allow_diagonal(req.allow_diagonal)
.allow_reverse(req.allow_reverse)
.seed(req.seed)
.show_solution(true)
.columns_word_list(req.columns_word_list)
.build()
.map_err(|e| JsValue::from_str(&format!("invalid solution parameters: {e}")))?;
let request = RenderRequest {
template_id: "default".into(),
pack_id: None,
title: None,
subtitle: None,
theme: None,
components: vec![
json!({
"type": "section",
"data": { "title": req.title, "level": 2, "content": [] }
}),
json!({
"type": "textblock",
"data": { "content": copy.explanation }
}),
json!({
"type": "callout",
"data": {
"content": copy.about_content,
"callout_type": "info",
"title": copy.about_title,
}
}),
json!({ "type": "page-break", "data": {} }),
json!({ "type": "word-search", "data": puzzle.to_data() }),
json!({ "type": "page-break", "data": {} }),
json!({ "type": "word-search", "data": solution.to_data() }),
],
assets: Default::default(),
metadata: [
("footer_prefix".to_string(), copy.footer_prefix.to_string()),
("author".to_string(), "renderreport".to_string()),
(
"footer_link_url".to_string(),
"https://renderreport.casoon.de".to_string(),
),
(
"footer_tagline".to_string(),
copy.footer_tagline.to_string(),
),
]
.into_iter()
.collect(),
page_setup: Default::default(),
};
wasm_engine()?
.render_pdf(&request)
.map_err(|e| JsValue::from_str(&format!("render failed: {e}")))
}