Skip to main content

hmd_wasm/
lib.rs

1use hmd_core::{validate_document_with_profiles, HmdDocument, ProfileDescriptor};
2use hmd_interaction::{apply_intent_json as apply_hmd_intent_json, create_patch_from_json};
3use hmd_patch::{apply_patch as apply_hmd_patch, HmdPatch};
4use hmd_render_html::{render_document, RenderOptions, RoundtripMode};
5use serde::{Deserialize, Serialize};
6use wasm_bindgen::prelude::*;
7
8#[derive(Debug, Serialize)]
9#[serde(rename_all = "camelCase")]
10struct ValidationResult {
11    valid: bool,
12    document: HmdDocument,
13}
14
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17struct WasmRenderOptions {
18    roundtrip: Option<String>,
19}
20
21#[wasm_bindgen(js_name = parse)]
22pub fn parse(source: &str) -> Result<String, JsValue> {
23    let document = hmd_parse::parse_document(source);
24    json_string(&document)
25}
26
27#[wasm_bindgen(js_name = validate)]
28pub fn validate(source: &str) -> Result<String, JsValue> {
29    let mut document = hmd_parse::parse_document(source);
30    validate_official_profiles(&mut document);
31    let result = ValidationResult {
32        valid: !document.has_error_diagnostics(),
33        document,
34    };
35    json_string(&result)
36}
37
38#[wasm_bindgen(js_name = renderHtml)]
39pub fn render_html(source: &str, options: Option<String>) -> Result<String, JsValue> {
40    let mut document = hmd_parse::parse_document(source);
41    validate_official_profiles(&mut document);
42    let options = render_options(source, options)?;
43    Ok(render_document(&document, options))
44}
45
46#[wasm_bindgen(js_name = applyPatch)]
47pub fn apply_patch(source: &str, patch_json: &str) -> Result<String, JsValue> {
48    let patch = serde_json::from_str::<HmdPatch>(patch_json)
49        .map_err(|error| JsValue::from_str(&format!("invalid patch JSON: {error}")))?;
50    let patched = apply_hmd_patch(source, &patch)
51        .map_err(|error| JsValue::from_str(&format!("failed to apply patch: {error}")))?;
52    validate_patched_source(&patched)?;
53    Ok(patched)
54}
55
56#[wasm_bindgen(js_name = createPatch)]
57pub fn create_patch(source: &str, intent_json: &str) -> Result<String, JsValue> {
58    let patch = create_patch_from_json(source, intent_json)
59        .map_err(|error| JsValue::from_str(&format!("failed to create patch: {error}")))?;
60    serde_json::to_string_pretty(&patch)
61        .map_err(|error| JsValue::from_str(&format!("failed to serialize patch JSON: {error}")))
62}
63
64#[wasm_bindgen(js_name = applyIntent)]
65pub fn apply_intent(source: &str, intent_json: &str) -> Result<String, JsValue> {
66    let patched = apply_hmd_intent_json(source, intent_json)
67        .map_err(|error| JsValue::from_str(&format!("failed to apply intent: {error}")))?;
68    validate_patched_source(&patched)?;
69    Ok(patched)
70}
71
72fn validate_official_profiles(document: &mut HmdDocument) {
73    validate_document_with_profiles(document, &official_descriptors());
74}
75
76fn validate_patched_source(source: &str) -> Result<(), JsValue> {
77    let mut document = hmd_parse::parse_document(source);
78    validate_official_profiles(&mut document);
79    if !document.has_error_diagnostics() {
80        return Ok(());
81    }
82
83    let errors = document
84        .diagnostics
85        .iter()
86        .filter(|diagnostic| diagnostic.severity == hmd_core::DiagnosticSeverity::Error)
87        .map(|diagnostic| format!("{}: {}", diagnostic.code, diagnostic.message))
88        .collect::<Vec<_>>()
89        .join("; ");
90    Err(JsValue::from_str(&format!(
91        "patched HMD failed validation: {errors}"
92    )))
93}
94
95fn official_descriptors() -> [ProfileDescriptor; 4] {
96    [
97        hmd_profile_general::descriptor(),
98        hmd_profile_decision::descriptor(),
99        hmd_profile_progress::descriptor(),
100        hmd_profile_todo::descriptor(),
101    ]
102}
103
104fn render_options(source: &str, options: Option<String>) -> Result<RenderOptions, JsValue> {
105    let mut render_options = RenderOptions::default().with_source(source);
106    let Some(options) = options else {
107        return Ok(render_options);
108    };
109    if options.trim().is_empty() {
110        return Ok(render_options);
111    }
112
113    let parsed: WasmRenderOptions = serde_json::from_str(&options)
114        .map_err(|error| JsValue::from_str(&format!("invalid render options JSON: {error}")))?;
115    if let Some(roundtrip) = parsed.roundtrip {
116        render_options = render_options.with_roundtrip(parse_roundtrip_mode(&roundtrip)?);
117    }
118    Ok(render_options)
119}
120
121fn parse_roundtrip_mode(value: &str) -> Result<RoundtripMode, JsValue> {
122    match value {
123        "view" => Ok(RoundtripMode::View),
124        "semantic" => Ok(RoundtripMode::Semantic),
125        "exact" => Ok(RoundtripMode::Exact),
126        _ => Err(JsValue::from_str(&format!(
127            "unknown roundtrip mode '{value}', expected view, semantic, or exact"
128        ))),
129    }
130}
131
132fn json_string<T: Serialize>(value: &T) -> Result<String, JsValue> {
133    serde_json::to_string(value)
134        .map_err(|error| JsValue::from_str(&format!("failed to serialize JSON: {error}")))
135}