use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use crate::async_runtime::WasmRuntime;
#[cfg(not(target_arch = "wasm32"))]
type WasmRuntime = crate::async_runtime::DefaultRuntime;
use crate::{AnalyzeError, Analyzer, PageQualityReport};
#[wasm_bindgen]
pub struct WasmAnalyzer {
analyzer: Analyzer<WasmRuntime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct WasmAnalyzerConfig {
profile: Option<String>,
enable_nlp: bool,
add_report: bool,
}
impl WasmAnalyzerConfig {
fn new() -> Self {
WasmAnalyzerConfig {
profile: None,
enable_nlp: true,
add_report: true,
}
}
}
impl Default for WasmAnalyzerConfig {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmAnalyzer {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<WasmAnalyzer, JsValue> {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let analyzer = Analyzer::<WasmRuntime>::builder()
.build()
.map_err(|e| JsValue::from_str(&format!("Failed to create analyzer: {}", e)))?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen(js_name = withConfig)]
pub fn with_config(
profile: Option<String>,
enable_nlp: Option<bool>,
add_report: Option<bool>,
) -> Result<WasmAnalyzer, JsValue> {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let mut builder = Analyzer::<WasmRuntime>::builder();
if let Some(profile_name) = profile {
builder = builder
.with_profile_name(&profile_name)
.map_err(|e| JsValue::from_str(&format!("Invalid profile: {}", e)))?;
}
#[cfg(feature = "nlp")]
if let Some(nlp) = enable_nlp {
builder = builder.enable_nlp(nlp);
}
if let Some(report) = add_report {
builder = builder.add_report(report);
}
let analyzer = builder
.build()
.map_err(|e| JsValue::from_str(&format!("Failed to create analyzer: {}", e)))?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen]
pub async fn analyze(&self, html: String) -> Result<JsValue, JsValue> {
let report = self
.analyzer
.run("http://unknown.local", Some(&html))
.await
.map_err(|e| convert_error(e))?;
serde_wasm_bindgen::to_value(&report)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))
}
#[wasm_bindgen(js_name = analyzeWithUrl)]
pub async fn analyze_with_url(&self, url: String, html: String) -> Result<JsValue, JsValue> {
let report = self
.analyzer
.run(&url, Some(&html))
.await
.map_err(|e| convert_error(e))?;
serde_wasm_bindgen::to_value(&report)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))
}
#[wasm_bindgen(js_name = fetchAndAnalyze)]
pub async fn fetch_and_analyze(&self, url: String) -> Result<JsValue, JsValue> {
let report = self
.analyzer
.run(&url, None) .await
.map_err(|e| convert_error(e))?;
serde_wasm_bindgen::to_value(&report)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))
}
#[wasm_bindgen(js_name = getAvailableProfiles)]
pub fn get_available_profiles() -> Vec<String> {
vec![
"general".to_string(),
"news".to_string(),
"content_article".to_string(),
"blog".to_string(),
"product".to_string(),
"login_page".to_string(),
"homepage".to_string(),
"portfolio".to_string(),
]
}
#[wasm_bindgen(js_name = getVersion)]
pub fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
}
#[wasm_bindgen]
pub struct WasmBatchResult {
results: Vec<Result<PageQualityReport, AnalyzeError>>,
}
#[wasm_bindgen]
impl WasmBatchResult {
#[wasm_bindgen(js_name = getCount)]
pub fn get_count(&self) -> usize {
self.results.len()
}
#[wasm_bindgen(js_name = getSuccessCount)]
pub fn get_success_count(&self) -> usize {
self.results.iter().filter(|r| r.is_ok()).count()
}
#[wasm_bindgen(js_name = getFailureCount)]
pub fn get_failure_count(&self) -> usize {
self.results.iter().filter(|r| r.is_err()).count()
}
#[wasm_bindgen(js_name = getResults)]
pub fn get_results(&self) -> Result<JsValue, JsValue> {
#[derive(Serialize)]
#[serde(tag = "success")]
enum BatchItem {
#[serde(rename = "true")]
Success { report: PageQualityReport },
#[serde(rename = "false")]
Failure { error: String },
}
let items: Vec<BatchItem> = self
.results
.iter()
.map(|result| match result {
Ok(report) => BatchItem::Success {
report: report.clone(),
},
Err(e) => BatchItem::Failure {
error: e.to_string(),
},
})
.collect();
serde_wasm_bindgen::to_value(&items)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))
}
}
#[wasm_bindgen]
pub struct WasmBatchAnalyzer {
analyzer: Analyzer<WasmRuntime>,
}
#[wasm_bindgen]
impl WasmBatchAnalyzer {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<WasmBatchAnalyzer, JsValue> {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let analyzer = Analyzer::<WasmRuntime>::builder()
.build()
.map_err(|e| JsValue::from_str(&format!("Failed to create analyzer: {}", e)))?;
Ok(WasmBatchAnalyzer { analyzer })
}
#[wasm_bindgen(js_name = analyzeBatch)]
pub async fn analyze_batch(
&self,
html_documents: Vec<String>,
) -> Result<WasmBatchResult, JsValue> {
let mut results = Vec::new();
for html in html_documents {
let result = self.analyzer.run("unknown", Some(&html)).await;
results.push(result);
}
Ok(WasmBatchResult { results })
}
}
#[wasm_bindgen]
pub struct WasmFieldSelector {
include_fields: Vec<String>,
exclude_fields: Vec<String>,
include_sections: Vec<String>,
exclude_sections: Vec<String>,
}
#[wasm_bindgen]
impl WasmFieldSelector {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmFieldSelector {
WasmFieldSelector {
include_fields: Vec::new(),
exclude_fields: Vec::new(),
include_sections: Vec::new(),
exclude_sections: Vec::new(),
}
}
#[wasm_bindgen(js_name = includeFields)]
pub fn include_fields(mut self, fields: Vec<String>) -> Self {
self.include_fields.extend(fields);
self
}
#[wasm_bindgen(js_name = excludeFields)]
pub fn exclude_fields(mut self, fields: Vec<String>) -> Self {
self.exclude_fields.extend(fields);
self
}
#[wasm_bindgen(js_name = includeSections)]
pub fn include_sections(mut self, sections: Vec<String>) -> Self {
self.include_sections.extend(sections);
self
}
#[wasm_bindgen(js_name = excludeSections)]
pub fn exclude_sections(mut self, sections: Vec<String>) -> Self {
self.exclude_sections.extend(sections);
self
}
fn to_field_selector(&self) -> crate::utils::json_optimizer::FieldSelector {
use crate::utils::json_optimizer::FieldSelector;
let mut builder = FieldSelector::builder();
if !self.include_fields.is_empty() {
builder = builder.include_fields(self.include_fields.clone());
}
if !self.exclude_fields.is_empty() {
builder = builder.exclude_fields(self.exclude_fields.clone());
}
if !self.include_sections.is_empty() {
builder = builder.include_sections(self.include_sections.clone());
}
if !self.exclude_sections.is_empty() {
builder = builder.exclude_sections(self.exclude_sections.clone());
}
builder.build()
}
}
#[wasm_bindgen]
impl WasmAnalyzer {
#[wasm_bindgen(js_name = analyzeWithFields)]
pub async fn analyze_with_fields(
&self,
html: String,
fields: Vec<String>,
) -> Result<JsValue, JsValue> {
use crate::utils::json_optimizer::{
FieldSelector, OptimizedSerializer, SerializationOptions,
};
let report = self
.analyzer
.run("unknown", Some(&html))
.await
.map_err(|e| convert_error(e))?;
let selector = FieldSelector::builder().include_fields(fields).build();
let opts = SerializationOptions::minimal();
let json = OptimizedSerializer::serialize_with_selector(&report, &selector, Some(&opts))
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))?;
let json_value: serde_json::Value = serde_json::from_str(&json)
.map_err(|e| JsValue::from_str(&format!("JSON parse failed: {}", e)))?;
serde_wasm_bindgen::to_value(&json_value)
.map_err(|e| JsValue::from_str(&format!("WASM serialization failed: {}", e)))
}
#[wasm_bindgen(js_name = analyzeWithSelector)]
pub async fn analyze_with_selector(
&self,
html: String,
selector: &WasmFieldSelector,
) -> Result<JsValue, JsValue> {
use crate::utils::json_optimizer::{OptimizedSerializer, SerializationOptions};
let report = self
.analyzer
.run("unknown", Some(&html))
.await
.map_err(|e| convert_error(e))?;
let field_selector = selector.to_field_selector();
let opts = SerializationOptions::minimal();
let json =
OptimizedSerializer::serialize_with_selector(&report, &field_selector, Some(&opts))
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {}", e)))?;
let json_value: serde_json::Value = serde_json::from_str(&json)
.map_err(|e| JsValue::from_str(&format!("JSON parse failed: {}", e)))?;
serde_wasm_bindgen::to_value(&json_value)
.map_err(|e| JsValue::from_str(&format!("WASM serialization failed: {}", e)))
}
}
#[wasm_bindgen]
impl WasmBatchAnalyzer {
#[wasm_bindgen(js_name = analyzeBatchWithFields)]
pub async fn analyze_batch_with_fields(
&self,
html_documents: Vec<String>,
fields: Vec<String>,
) -> Result<JsValue, JsValue> {
use crate::utils::json_optimizer::{
FieldSelector, OptimizedSerializer, SerializationOptions,
};
let mut reports = Vec::new();
for html in html_documents {
match self.analyzer.run("unknown", Some(&html)).await {
Ok(report) => reports.push(report),
Err(_) => continue, }
}
let selector = FieldSelector::builder().include_fields(fields).build();
let opts = SerializationOptions {
compact: true,
skip_empty_fields: true,
minimal_output: false,
streaming: true,
buffer_size: 16384,
};
let json =
OptimizedSerializer::serialize_batch_with_selector(&reports, &selector, Some(&opts))
.map_err(|e| JsValue::from_str(&format!("Batch serialization failed: {}", e)))?;
let json_value: serde_json::Value = serde_json::from_str(&json)
.map_err(|e| JsValue::from_str(&format!("JSON parse failed: {}", e)))?;
serde_wasm_bindgen::to_value(&json_value)
.map_err(|e| JsValue::from_str(&format!("WASM serialization failed: {}", e)))
}
}
#[wasm_bindgen]
impl WasmAnalyzer {
#[wasm_bindgen(js_name = fromConfig)]
pub fn from_config(config_json: String) -> Result<WasmAnalyzer, JsValue> {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let config_value: serde_json::Value = serde_json::from_str(&config_json)
.map_err(|e| JsValue::from_str(&format!("Invalid JSON: {}", e)))?;
let profile_name = config_value["profile"].as_str().unwrap_or("general");
let analyzer = Analyzer::<WasmRuntime>::builder()
.with_profile_name(profile_name)
.map_err(|e| JsValue::from_str(&format!("Failed to set profile: {}", e)))?
.build()
.map_err(|e| JsValue::from_str(&format!("Failed to create analyzer: {}", e)))?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen(js_name = getConfig)]
pub fn get_config(&self) -> Result<String, JsValue> {
Err(JsValue::from_str(
"getConfig not yet implemented - requires Analyzer to expose config",
))
}
}
#[wasm_bindgen]
pub struct WasmAnalyzerBuilder {
profile: Option<String>,
enabled_metrics: Vec<String>,
disabled_metrics: Vec<String>,
custom_thresholds: Vec<(String, f32, f32, f32, f32)>, metric_weights: Vec<(String, f32)>, custom_penalties: Vec<crate::config::enhanced_models::GlobalPenalty>, custom_bonuses: Vec<crate::config::enhanced_models::GlobalBonus>, enable_nlp: bool,
enable_link_check: bool, add_report: bool,
}
#[wasm_bindgen]
impl WasmAnalyzerBuilder {
#[wasm_bindgen(constructor)]
pub fn new() -> WasmAnalyzerBuilder {
WasmAnalyzerBuilder {
profile: None,
enabled_metrics: Vec::new(),
disabled_metrics: Vec::new(),
custom_thresholds: Vec::new(),
metric_weights: Vec::new(), custom_penalties: Vec::new(), custom_bonuses: Vec::new(), enable_nlp: true,
enable_link_check: false, add_report: true,
}
}
#[wasm_bindgen(js_name = withProfile)]
pub fn with_profile(mut self, profile: String) -> Self {
self.profile = Some(profile);
self
}
#[wasm_bindgen(js_name = enableMetric)]
pub fn enable_metric(mut self, metric_name: String) -> Self {
self.enabled_metrics.push(metric_name);
self
}
#[wasm_bindgen(js_name = disableMetric)]
pub fn disable_metric(mut self, metric_name: String) -> Self {
self.disabled_metrics.push(metric_name);
self
}
#[wasm_bindgen(js_name = setThreshold)]
pub fn set_threshold(
mut self,
metric_name: String,
min: f32,
optimal_min: f32,
optimal_max: f32,
max: f32,
) -> Self {
self.custom_thresholds
.push((metric_name, min, optimal_min, optimal_max, max));
self
}
#[wasm_bindgen(js_name = setSimpleThreshold)]
pub fn set_simple_threshold(
mut self,
metric_name: String,
min: f32,
optimal: f32,
max: f32,
) -> Self {
self.custom_thresholds
.push((metric_name, min, optimal, optimal, max));
self
}
#[wasm_bindgen(js_name = setMetricWeight)]
pub fn set_metric_weight(mut self, metric_name: String, weight: f32) -> Self {
self.metric_weights.push((metric_name, weight));
self
}
#[wasm_bindgen(js_name = addPenaltyBelow)]
pub fn add_penalty_below(
mut self,
penalty_id: String,
metric: String,
threshold: f32,
penalty_points: f32,
description: String,
) -> Self {
use crate::config::enhanced_models::{GlobalPenalty, PenaltyTrigger, PenaltyType};
let penalty = GlobalPenalty {
trigger_condition: PenaltyTrigger::MetricBelow { metric, threshold },
penalty_type: PenaltyType::FixedPoints {
points: penalty_points,
},
description: format!("{} (ID: {})", description, penalty_id),
enabled: true,
};
self.custom_penalties.push(penalty);
self
}
#[wasm_bindgen(js_name = addPenaltyAbove)]
pub fn add_penalty_above(
mut self,
penalty_id: String,
metric: String,
threshold: f32,
penalty_points: f32,
description: String,
) -> Self {
use crate::config::enhanced_models::{GlobalPenalty, PenaltyTrigger, PenaltyType};
let penalty = GlobalPenalty {
trigger_condition: PenaltyTrigger::MetricAbove { metric, threshold },
penalty_type: PenaltyType::FixedPoints {
points: penalty_points,
},
description: format!("{} (ID: {})", description, penalty_id),
enabled: true,
};
self.custom_penalties.push(penalty);
self
}
#[wasm_bindgen(js_name = addBonusAbove)]
pub fn add_bonus_above(
mut self,
bonus_id: String,
metric: String,
threshold: f32,
bonus_points: f32,
description: String,
) -> Self {
use crate::config::enhanced_models::{BonusTrigger, GlobalBonus};
let bonus = GlobalBonus {
trigger_condition: BonusTrigger::MetricExcellence { metric, threshold },
bonus_points,
description: format!("{} (ID: {})", description, bonus_id),
enabled: true,
};
self.custom_bonuses.push(bonus);
self
}
#[wasm_bindgen(js_name = addBonusMultiple)]
pub fn add_bonus_multiple(
mut self,
bonus_id: String,
metrics: Vec<String>,
threshold: f32,
bonus_points: f32,
description: String,
) -> Self {
use crate::config::enhanced_models::{BonusTrigger, GlobalBonus};
let bonus = GlobalBonus {
trigger_condition: BonusTrigger::MultipleMetricsGood { metrics, threshold },
bonus_points,
description: format!("{} (ID: {})", description, bonus_id),
enabled: true,
};
self.custom_bonuses.push(bonus);
self
}
#[wasm_bindgen(js_name = enableNlp)]
pub fn enable_nlp(mut self, enable: bool) -> Self {
self.enable_nlp = enable;
self
}
#[wasm_bindgen(js_name = enableLinkCheck)]
pub fn enable_link_check(mut self, enable: bool) -> Self {
self.enable_link_check = enable;
self
}
#[wasm_bindgen(js_name = addReport)]
pub fn add_report(mut self, add: bool) -> Self {
self.add_report = add;
self
}
#[wasm_bindgen(js_name = fromConfigJson)]
pub fn from_config_json(config_json: &str) -> Result<WasmAnalyzer, JsValue> {
use crate::config::config_manager::{ConfigFormat, ConfigManager};
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let config_manager = ConfigManager::from_str(config_json, ConfigFormat::Json)
.map_err(|e| JsValue::from_str(&format!("Failed to parse JSON config: {}", e)))?;
let analyzer = Analyzer::<WasmRuntime>::builder()
.with_config_manager(config_manager)
.build()
.map_err(|e| {
JsValue::from_str(&format!("Failed to build analyzer from config: {}", e))
})?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen(js_name = fromConfigYaml)]
pub fn from_config_yaml(config_yaml: &str) -> Result<WasmAnalyzer, JsValue> {
use crate::config::config_manager::{ConfigFormat, ConfigManager};
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let config_manager = ConfigManager::from_str(config_yaml, ConfigFormat::Yaml)
.map_err(|e| JsValue::from_str(&format!("Failed to parse YAML config: {}", e)))?;
let analyzer = Analyzer::<WasmRuntime>::builder()
.with_config_manager(config_manager)
.build()
.map_err(|e| {
JsValue::from_str(&format!("Failed to build analyzer from config: {}", e))
})?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen(js_name = fromConfigToml)]
pub fn from_config_toml(config_toml: &str) -> Result<WasmAnalyzer, JsValue> {
use crate::config::config_manager::{ConfigFormat, ConfigManager};
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let config_manager = ConfigManager::from_str(config_toml, ConfigFormat::Toml)
.map_err(|e| JsValue::from_str(&format!("Failed to parse TOML config: {}", e)))?;
let analyzer = Analyzer::<WasmRuntime>::builder()
.with_config_manager(config_manager)
.build()
.map_err(|e| {
JsValue::from_str(&format!("Failed to build analyzer from config: {}", e))
})?;
Ok(WasmAnalyzer { analyzer })
}
#[wasm_bindgen]
pub fn build(self) -> Result<WasmAnalyzer, JsValue> {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
let mut builder = Analyzer::<WasmRuntime>::builder();
if let Some(profile_name) = self.profile {
builder = builder.with_profile_name(&profile_name).map_err(|e| {
JsValue::from_str(&format!("Invalid profile '{}': {}", profile_name, e))
})?;
}
for metric_name in &self.enabled_metrics {
builder = builder.enable_metric(metric_name).map_err(|e| {
JsValue::from_str(&format!("Failed to enable metric '{}': {}", metric_name, e))
})?;
}
for metric_name in &self.disabled_metrics {
builder = builder.disable_metric(metric_name).map_err(|e| {
JsValue::from_str(&format!(
"Failed to disable metric '{}': {}",
metric_name, e
))
})?;
}
for (metric_name, min, optimal_min, optimal_max, max) in self.custom_thresholds {
builder = builder
.set_metric_threshold(&metric_name, min, optimal_min, optimal_max, max)
.map_err(|e| {
JsValue::from_str(&format!(
"Failed to set threshold for '{}': {}",
metric_name, e
))
})?;
}
for (metric_name, weight) in self.metric_weights {
builder = builder
.set_metric_weight(&metric_name, weight)
.map_err(|e| {
JsValue::from_str(&format!(
"Failed to set weight for '{}': {}",
metric_name, e
))
})?;
}
for penalty in self.custom_penalties {
builder = builder
.add_penalty(penalty)
.map_err(|e| JsValue::from_str(&format!("Failed to add penalty: {}", e)))?;
}
for bonus in self.custom_bonuses {
builder = builder
.add_bonus(bonus)
.map_err(|e| JsValue::from_str(&format!("Failed to add bonus: {}", e)))?;
}
#[cfg(feature = "nlp")]
{
builder = builder.enable_nlp(self.enable_nlp);
}
#[cfg(feature = "linkcheck")]
{
builder = builder.enable_linkcheck(self.enable_link_check);
}
builder = builder.add_report(self.add_report);
let analyzer = builder
.build()
.map_err(|e| JsValue::from_str(&format!("Failed to build analyzer: {}", e)))?;
Ok(WasmAnalyzer { analyzer })
}
}
#[wasm_bindgen]
impl WasmAnalyzer {
#[wasm_bindgen]
pub fn builder() -> WasmAnalyzerBuilder {
WasmAnalyzerBuilder::new()
}
}
fn convert_error(error: AnalyzeError) -> JsValue {
let error_msg = match error {
AnalyzeError::InvalidUrl(msg) => format!("Invalid URL: {}", msg),
AnalyzeError::NetworkError(msg) => format!("Network error: {}", msg),
AnalyzeError::ParseError(msg) => format!("Parse error: {}", msg),
AnalyzeError::ConfigError(msg) => format!("Configuration error: {}", msg),
AnalyzeError::AnalysisError(msg) => format!("Analysis error: {}", msg),
_ => format!("Error: {}", error),
};
JsValue::from_str(&error_msg)
}
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
#[cfg(feature = "wasm-logger")]
wasm_logger::init(wasm_logger::Config::default());
}
#[wasm_bindgen(js_name = getVersion)]
pub fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen(js_name = getVersionChangelog)]
pub fn get_version_changelog() -> String {
format!(
"v{} - Profile scoring improvements:\n\
- Product profile: Reverted to e-commerce focus (breaking change)\n\
- Login page: Stricter validation (Forms: 40% weight)\n\
- Homepage/General: More balanced scoring with new bonuses\n\
- Fixed: MetricEquals penalty trigger implementation\n\
See CHANGELOG.md for full details.",
env!("CARGO_PKG_VERSION")
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_creation() {
let config = WasmAnalyzerConfig::new();
assert_eq!(config.profile, None);
assert!(config.enable_nlp);
assert!(config.add_report);
}
#[test]
fn test_version() {
let version = get_version();
assert!(version.starts_with("1.0"));
}
#[test]
fn test_profile_list() {
let profiles = WasmAnalyzer::get_available_profiles();
assert!(profiles.contains(&"default".to_string()));
assert!(profiles.contains(&"news".to_string()));
assert!(profiles.len() >= 6);
}
}