use hmd_core::{validate_document_with_profiles, HmdDocument, ProfileDescriptor};
use hmd_interaction::{apply_intent_json as apply_hmd_intent_json, create_patch_from_json};
use hmd_patch::{apply_patch as apply_hmd_patch, HmdPatch};
use hmd_render_html::{render_document, RenderOptions, RoundtripMode};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ValidationResult {
valid: bool,
document: HmdDocument,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct WasmRenderOptions {
roundtrip: Option<String>,
}
#[wasm_bindgen(js_name = parse)]
pub fn parse(source: &str) -> Result<String, JsValue> {
let document = hmd_parse::parse_document(source);
json_string(&document)
}
#[wasm_bindgen(js_name = validate)]
pub fn validate(source: &str) -> Result<String, JsValue> {
let mut document = hmd_parse::parse_document(source);
validate_official_profiles(&mut document);
let result = ValidationResult {
valid: !document.has_error_diagnostics(),
document,
};
json_string(&result)
}
#[wasm_bindgen(js_name = renderHtml)]
pub fn render_html(source: &str, options: Option<String>) -> Result<String, JsValue> {
let mut document = hmd_parse::parse_document(source);
validate_official_profiles(&mut document);
let options = render_options(source, options)?;
Ok(render_document(&document, options))
}
#[wasm_bindgen(js_name = applyPatch)]
pub fn apply_patch(source: &str, patch_json: &str) -> Result<String, JsValue> {
let patch = serde_json::from_str::<HmdPatch>(patch_json)
.map_err(|error| JsValue::from_str(&format!("invalid patch JSON: {error}")))?;
let patched = apply_hmd_patch(source, &patch)
.map_err(|error| JsValue::from_str(&format!("failed to apply patch: {error}")))?;
validate_patched_source(&patched)?;
Ok(patched)
}
#[wasm_bindgen(js_name = createPatch)]
pub fn create_patch(source: &str, intent_json: &str) -> Result<String, JsValue> {
let patch = create_patch_from_json(source, intent_json)
.map_err(|error| JsValue::from_str(&format!("failed to create patch: {error}")))?;
serde_json::to_string_pretty(&patch)
.map_err(|error| JsValue::from_str(&format!("failed to serialize patch JSON: {error}")))
}
#[wasm_bindgen(js_name = applyIntent)]
pub fn apply_intent(source: &str, intent_json: &str) -> Result<String, JsValue> {
let patched = apply_hmd_intent_json(source, intent_json)
.map_err(|error| JsValue::from_str(&format!("failed to apply intent: {error}")))?;
validate_patched_source(&patched)?;
Ok(patched)
}
fn validate_official_profiles(document: &mut HmdDocument) {
validate_document_with_profiles(document, &official_descriptors());
}
fn validate_patched_source(source: &str) -> Result<(), JsValue> {
let mut document = hmd_parse::parse_document(source);
validate_official_profiles(&mut document);
if !document.has_error_diagnostics() {
return Ok(());
}
let errors = document
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.severity == hmd_core::DiagnosticSeverity::Error)
.map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message))
.collect::<Vec<_>>()
.join("; ");
Err(JsValue::from_str(&format!(
"patched HMD failed validation: {errors}"
)))
}
fn official_descriptors() -> [ProfileDescriptor; 4] {
[
hmd_profile_general::descriptor(),
hmd_profile_decision::descriptor(),
hmd_profile_progress::descriptor(),
hmd_profile_todo::descriptor(),
]
}
fn render_options(source: &str, options: Option<String>) -> Result<RenderOptions, JsValue> {
let mut render_options = RenderOptions::default().with_source(source);
let Some(options) = options else {
return Ok(render_options);
};
if options.trim().is_empty() {
return Ok(render_options);
}
let parsed: WasmRenderOptions = serde_json::from_str(&options)
.map_err(|error| JsValue::from_str(&format!("invalid render options JSON: {error}")))?;
if let Some(roundtrip) = parsed.roundtrip {
render_options = render_options.with_roundtrip(parse_roundtrip_mode(&roundtrip)?);
}
Ok(render_options)
}
fn parse_roundtrip_mode(value: &str) -> Result<RoundtripMode, JsValue> {
match value {
"view" => Ok(RoundtripMode::View),
"semantic" => Ok(RoundtripMode::Semantic),
"exact" => Ok(RoundtripMode::Exact),
_ => Err(JsValue::from_str(&format!(
"unknown roundtrip mode '{value}', expected view, semantic, or exact"
))),
}
}
fn json_string<T: Serialize>(value: &T) -> Result<String, JsValue> {
serde_json::to_string(value)
.map_err(|error| JsValue::from_str(&format!("failed to serialize JSON: {error}")))
}