crates/laymesh-wasm/src/lib.rs 1//! Browser bindings use the same parser, layout, fonts, and SVG renderer as native.2use laymesh_core::{3 engine::compile_source,4 model::{Host, Scene},5};6use laymesh_language::{LanguageService, byte_offset, utf16_offset};7use serde_json::{Value as J, json};8use std::cell::RefCell;9use std::collections::BTreeMap;10use wasm_bindgen::prelude::*;11thread_local! {12 static PREVIEW_FONTS: RefCell<BTreeMap<String, Vec<u8>>> = RefCell::new(BTreeMap::new());13}14 15/// Register Page-only body fonts as bytes, avoiding a large JSON byte array on every render.16#[wasm_bindgen]17pub fn register_preview_font(filename: &str, bytes: &[u8]) {18 PREVIEW_FONTS.with(|fonts| {19 fonts20 .borrow_mut()21 .insert(filename.to_owned(), bytes.to_vec());22 });23}24/// A scene remains in WASM memory between layout and SVG generation, avoiding25/// serialization of embedded fonts just to measure the two stages separately.26#[wasm_bindgen]27pub struct RenderJob {28 scene: Scene,29}30#[wasm_bindgen]31impl RenderJob {32 pub fn svg(&self) -> Result<String, JsValue> {33 laymesh_render::render_svg(&self.scene).map_err(|e| JsValue::from_str(&e.to_string()))34 }35 pub fn inspection(&self) -> String {36 json!({"inspection":laymesh_language::inspect::inspect_scene(&self.scene),"warnings":self.scene.warnings}).to_string()37 }38}39#[wasm_bindgen]40pub fn prepare_render(41 source: &str,42 filename: &str,43 files_json: &str,44) -> Result<RenderJob, JsValue> {45 let files: J =46 serde_json::from_str(files_json).map_err(|e| JsValue::from_str(&e.to_string()))?;47 let mut host = Host {48 native: false,49 ..Host::default()50 };51 if let Some(files) = files.as_object() {52 for (name, value) in files {53 let data = if let Some(s) = value.as_str() {54 s.as_bytes().to_vec()55 } else if let Some(a) = value.as_array() {56 a.iter().map(|v| v.as_u64().unwrap_or(0) as u8).collect()57 } else {58 continue;59 };60 host.files.insert(name.clone(), data);61 }62 }63 PREVIEW_FONTS.with(|fonts| {64 host.files.extend(65 fonts66 .borrow()67 .iter()68 .map(|(name, data)| (name.clone(), data.clone())),69 );70 });71 let scene = compile_source(source, filename, host).map_err(|e| {72 let mut diagnostic = serde_json::to_value(&e).unwrap();73 // Native errors remain unchanged. This extra field is a browser resource74 // request; the worker only honors paths in its local resource manifest.75 if e.code == "E_ASSET" {76 if let Some(path) = e.message.strip_prefix("无法读取资源:") {77 diagnostic["resource"] = json!(path);78 }79 }80 JsValue::from_str(&diagnostic.to_string())81 })?;82 Ok(RenderJob { scene })83}84#[wasm_bindgen]85pub fn render(source: &str, filename: &str, files_json: &str) -> Result<String, JsValue> {86 let job = prepare_render(source, filename, files_json)?;87 let mut result: J = serde_json::from_str(&job.inspection()).unwrap();88 result["svg"] = json!(job.svg()?);89 Ok(result.to_string())90}91#[wasm_bindgen]92pub fn color_convert(input: &str) -> Result<String, JsValue> {93 let v: J = serde_json::from_str(input).map_err(|e| JsValue::from_str(&e.to_string()))?;94 let c = laymesh_core::color::Color::new(95 v["space"].as_str().unwrap_or("rgb"),96 [97 v["channels"][0].as_f64().unwrap_or(f64::NAN),98 v["channels"][1].as_f64().unwrap_or(f64::NAN),99 v["channels"][2].as_f64().unwrap_or(f64::NAN),100 ],101 v["alpha"].as_f64().unwrap_or(1.),102 )103 .map_err(|e| JsValue::from_str(&e))?;104 Ok(json!({"rgba":c.rgba,"mapped":c.mapped,"hex":c.hex(),"rgb":c.channels_in("rgb"),"hsv":c.channels_in("hsv"),"oklch":c.channels_in("oklch")}).to_string())105}106#[wasm_bindgen]107pub fn color_presentations(108 files: &str,109 filename: &str,110 from: usize,111 to: usize,112 rgba: &str,113) -> Result<String, JsValue> {114 let files: BTreeMap<String, String> =115 serde_json::from_str(files).map_err(|e| JsValue::from_str(&e.to_string()))?;116 let values: [f64; 4] =117 serde_json::from_str(rgba).map_err(|e| JsValue::from_str(&e.to_string()))?;118 let mut service = LanguageService::new("en");119 service.documents = files;120 let source = service121 .documents122 .get(filename)123 .map(String::as_str)124 .unwrap_or("");125 let mut result = service.color_presentations(126 filename,127 byte_offset(source, from),128 byte_offset(source, to),129 values,130 );131 for v in result.as_array_mut().unwrap() {132 v["from"] = json!(from);133 v["to"] = json!(to);134 }135 Ok(result.to_string())136}137#[wasm_bindgen]138pub fn language_query(139 files_json: &str,140 filename: &str,141 method: &str,142 utf16: usize,143 locale: &str,144) -> Result<String, JsValue> {145 let files: BTreeMap<String, String> =146 serde_json::from_str(files_json).map_err(|e| JsValue::from_str(&e.to_string()))?;147 let mut service = LanguageService::new(locale);148 service.documents = files;149 let source = service150 .documents151 .get(filename)152 .map(String::as_str)153 .unwrap_or("");154 let mut result = service.query(method, filename, byte_offset(source, utf16));155 fn convert(v: &mut J, source: &str, files: &BTreeMap<String, String>) {156 match v {157 J::Array(a) => {158 for x in a {159 convert(x, source, files)160 }161 }162 J::Object(m) => {163 let own = m164 .get("uri")165 .and_then(J::as_str)166 .and_then(|u| files.get(u))167 .map(String::as_str)168 .unwrap_or(source);169 for key in ["from", "to"] {170 if let Some(n) = m.get(key).and_then(J::as_u64) {171 m.insert(key.into(), json!(utf16_offset(own, n as usize)));172 }173 }174 for (key, value) in m.iter_mut() {175 if key != "from" && key != "to" {176 convert(value, own, files);177 }178 }179 }180 _ => {}181 }182 }183 convert(&mut result, source, &service.documents);184 Ok(result.to_string())185}