Skip to main content

webui_wasm/
parser.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Parser-only WASM exports.
5
6use crate::error::WasmError;
7use std::collections::HashMap;
8use wasm_bindgen::prelude::*;
9use webui_parser::plugin::webui::WebUIParserPlugin;
10use webui_parser::{CssStrategy, HtmlParser};
11use webui_protocol::WebUIProtocol;
12
13/// Build protocol protobuf bytes from virtual files without rendering.
14///
15/// Returns the serialized `WebUIProtocol` as protobuf bytes.
16#[wasm_bindgen]
17pub fn build_protocol(files: JsValue, entry: &str) -> Result<Vec<u8>, JsValue> {
18    let files_map: HashMap<String, String> =
19        serde_wasm_bindgen::from_value(files).map_err(|e| JsValue::from_str(&e.to_string()))?;
20
21    build_protocol_inner(&files_map, entry).map_err(|e| JsValue::from_str(&e.to_string()))
22}
23
24pub(crate) fn build_protocol_inner(
25    files: &HashMap<String, String>,
26    entry: &str,
27) -> Result<Vec<u8>, WasmError> {
28    let protocol = parse_to_protocol(files, entry)?;
29    protocol.to_protobuf().map_err(WasmError::Protocol)
30}
31
32/// Register all component `.html` files and optional companion `.css` files
33/// from the virtual file map, skipping the entry.
34fn register_components(
35    parser: &mut HtmlParser,
36    files: &HashMap<String, String>,
37    entry: &str,
38) -> Result<(), WasmError> {
39    for (filename, content) in files {
40        if filename != entry && filename.ends_with(".html") {
41            let tag_name = filename.trim_end_matches(".html");
42            if tag_name.contains('-') {
43                let css_key = format!("{tag_name}.css");
44                let css = files.get(&css_key).map(String::as_str);
45                parser.component_registry_mut().register_component(
46                    tag_name,
47                    content,
48                    css,
49                    has_script(files, tag_name),
50                )?;
51            }
52        }
53    }
54    Ok(())
55}
56
57fn has_script(files: &HashMap<String, String>, tag_name: &str) -> bool {
58    files.contains_key(&format!("{tag_name}.ts")) || files.contains_key(&format!("{tag_name}.js"))
59}
60
61/// Parse virtual files into a `WebUIProtocol` using the real `webui-parser`
62/// with the WebUI plugin.
63pub(crate) fn parse_to_protocol(
64    files: &HashMap<String, String>,
65    entry: &str,
66) -> Result<WebUIProtocol, WasmError> {
67    let entry_html = files
68        .get(entry)
69        .ok_or_else(|| WasmError::MissingEntry(entry.to_string()))?;
70
71    let mut parser =
72        HtmlParser::with_plugin_options(Box::new(WebUIParserPlugin::new()), CssStrategy::Style);
73    register_components(&mut parser, files, entry)?;
74    parser.parse(entry, entry_html)?;
75    parser.take_plugin_artifacts()?;
76
77    Ok(WebUIProtocol::new(parser.into_fragment_records()))
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn build_protocol_reports_missing_entry() {
86        let files = HashMap::new();
87        let err = build_protocol_inner(&files, "index.html").unwrap_err();
88        assert_eq!(err.to_string(), "Entry file 'index.html' not found");
89    }
90}