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}