mod customization;
pub(crate) mod template;
pub use customization::HtmlCustomization;
#[cfg(feature = "std")]
use std::{
collections::HashMap,
fs,
path::Path,
string::{String, ToString},
vec::Vec,
};
use super::types::{ComponentMeta, ErrorDoc, PrimaryMeta, SequenceMeta};
use crate::{Severity, traits::Role};
const CSS_VARIABLES: &str = include_str!("../../assets/css/variables.css");
const CSS_BASE: &str = include_str!("../../assets/css/base.css");
const CSS_LAYOUT: &str = include_str!("../../assets/css/layout.css");
const CSS_HEADER: &str = include_str!("../../assets/css/header.css");
const CSS_SEARCH: &str = include_str!("../../assets/css/search.css");
const CSS_CARDS: &str = include_str!("../../assets/css/cards.css");
const CSS_SIDEBAR: &str = include_str!("../../assets/css/sidebar.css");
const CSS_BUILDER: &str = include_str!("../../assets/css/builder.css");
const CSS_TOAST: &str = include_str!("../../assets/css/toast.css");
const CSS_RESPONSIVE: &str = include_str!("../../assets/css/responsive.css");
const JS_STATE: &str = include_str!("../../assets/js/state.js");
const JS_UTILS: &str = include_str!("../../assets/js/utils.js");
const JS_TOAST: &str = include_str!("../../assets/js/toast.js");
const JS_THEME: &str = include_str!("../../assets/js/theme.js");
const JS_VISIBILITY: &str = include_str!("../../assets/js/visibility.js");
const JS_SEARCH: &str = include_str!("../../assets/js/search.js");
const JS_RENDER: &str = include_str!("../../assets/js/render.js");
const JS_SIDEBAR: &str = include_str!("../../assets/js/sidebar.js");
const JS_BUILDER: &str = include_str!("../../assets/js/builder.js");
const JS_INIT: &str = include_str!("../../assets/js/init.js");
fn get_default_css() -> String {
[
CSS_VARIABLES,
CSS_BASE,
CSS_LAYOUT,
CSS_HEADER,
CSS_SEARCH,
CSS_CARDS,
CSS_SIDEBAR,
CSS_BUILDER,
CSS_TOAST,
CSS_RESPONSIVE,
]
.join("\n\n")
}
fn get_default_js() -> String {
[
JS_STATE,
JS_UTILS,
JS_TOAST,
JS_THEME,
JS_VISIBILITY,
JS_SEARCH,
JS_RENDER,
JS_SIDEBAR,
JS_BUILDER,
JS_INIT,
]
.join("\n\n")
}
fn parse_role(role_str: &str) -> Role {
match role_str {
"Public" => Role::Public,
"Developer" => Role::Developer,
"Internal" => Role::Internal,
_ => Role::Public, }
}
pub(super) fn is_visible_at_role(content_role: Option<&str>, viewing_role: Role) -> bool {
let content_role_enum = content_role.map(parse_role);
viewing_role.can_view(content_role_enum)
}
#[cfg(feature = "serde")]
fn generate_data_json(
project_name: &str,
version: &str,
errors: &[&ErrorDoc],
components: &HashMap<String, ComponentMeta>,
primaries: &HashMap<String, PrimaryMeta>,
sequences: &HashMap<u32, SequenceMeta>,
filter_role: Option<Role>,
) -> String {
use serde_json::json;
let target_role = filter_role.unwrap_or(Role::Internal);
let filtered_errors: HashMap<String, serde_json::Value> = errors
.iter()
.map(|e| {
#[cfg(feature = "runtime-hash")]
let error_json = json!({
"code": e.code,
"severity": e.severity,
"component": e.component,
"primary": e.primary,
"sequence": e.sequence,
"description": e.description,
"message": e.message,
"fields": e.fields,
"hash": e.hash,
"namespace": e.namespace,
"namespace_hash": e.namespace_hash,
"hints": e.hints_for_role(target_role),
"tags": e.tags_for_role(target_role),
"introduced": e.introduced,
"deprecated": e.deprecated,
"docs_url": e.docs_url,
"related_codes": e.related_codes_for_role(target_role),
"see_also": e.see_also_for_role(target_role),
"role": e.role,
"code_snippets": e.code_snippets,
});
#[cfg(not(feature = "runtime-hash"))]
let error_json = json!({
"code": e.code,
"severity": e.severity,
"component": e.component,
"primary": e.primary,
"sequence": e.sequence,
"description": e.description,
"message": e.message,
"fields": e.fields,
"namespace": e.namespace,
"hints": e.hints_for_role(target_role),
"tags": e.tags_for_role(target_role),
"introduced": e.introduced,
"deprecated": e.deprecated,
"docs_url": e.docs_url,
"related_codes": e.related_codes_for_role(target_role),
"see_also": e.see_also_for_role(target_role),
"role": e.role,
"code_snippets": e.code_snippets,
});
(e.code.clone(), error_json)
})
.collect();
let all_severities = [
Severity::Error,
Severity::Warning,
Severity::Critical,
Severity::Blocked,
Severity::Help,
Severity::Success,
Severity::Completed,
Severity::Info,
Severity::Trace,
];
let sev_data: HashMap<String, serde_json::Value> = all_severities
.iter()
.map(|s| {
let char_key = s.as_char().to_string();
let emoji = match s {
Severity::Error => "❌",
Severity::Warning => "⚠️",
Severity::Critical => "🔥",
Severity::Blocked => "🚫",
Severity::Help => "💡",
Severity::Success => "✅",
Severity::Completed => "✔️",
Severity::Info => "ℹ️",
Severity::Trace => "🔍",
};
let value = json!({
"name": s.as_str(),
"description": s.description(),
"color": s.hex_color(),
"bg_color": s.hex_bg_color(),
"emoji": emoji,
});
(char_key, value)
})
.collect();
let visible_codes: std::collections::HashSet<String> =
filtered_errors.keys().cloned().collect();
let filtered_components: HashMap<String, ComponentMeta> = components
.iter()
.map(|(name, comp)| {
let mut filtered_comp = comp.clone();
filtered_comp.errors = comp
.errors
.iter()
.filter(|code| visible_codes.contains(*code))
.cloned()
.collect();
filtered_comp.error_count = filtered_comp.errors.len();
(name.clone(), filtered_comp)
})
.collect();
let filtered_primaries: HashMap<String, PrimaryMeta> = primaries
.iter()
.map(|(name, prim)| {
let mut filtered_prim = prim.clone();
filtered_prim.errors = prim
.errors
.iter()
.filter(|code| visible_codes.contains(*code))
.cloned()
.collect();
filtered_prim.error_count = filtered_prim.errors.len();
(name.clone(), filtered_prim)
})
.collect();
let filtered_sequences: HashMap<u32, SequenceMeta> = sequences
.iter()
.map(|(key, seq)| {
let mut filtered_seq = seq.clone();
filtered_seq.errors = seq
.errors
.iter()
.filter(|code| visible_codes.contains(*code))
.cloned()
.collect();
filtered_seq.error_count = filtered_seq.errors.len();
(*key, filtered_seq)
})
.collect();
#[cfg(feature = "runtime-hash")]
let hash_lookup: HashMap<String, String> = errors
.iter()
.filter(|e| is_visible_at_role(e.role.as_deref(), filter_role.unwrap_or(Role::Internal)))
.filter_map(|e| e.hash.as_ref().map(|h| (h.clone(), e.code.clone())))
.collect();
#[cfg(feature = "runtime-hash")]
let combined = json!({
"_html_format_version": "2.0-hashmap",
"project": project_name,
"version": version,
"role": filter_role.map(|r| std::format!("{:?}", r)),
"errors": filtered_errors,
"components": filtered_components,
"primaries": filtered_primaries,
"sequences": filtered_sequences,
"#": hash_lookup,
"severities": sev_data,
});
#[cfg(not(feature = "runtime-hash"))]
let combined = json!({
"_html_format_version": "2.0-hashmap",
"project": project_name,
"version": version,
"role": filter_role.map(|r| std::format!("{:?}", r)),
"errors": filtered_errors,
"components": filtered_components,
"primaries": filtered_primaries,
"sequences": filtered_sequences,
"severities": sev_data,
});
serde_json::to_string(&combined).unwrap_or_else(|_| "{}".to_string())
}
#[cfg(feature = "serde")]
pub(super) struct HtmlConfig<'a> {
pub html_path: &'a Path,
pub project_name: &'a str,
pub version: &'a str,
pub errors: &'a [&'a ErrorDoc],
pub components: &'a HashMap<String, ComponentMeta>,
pub primaries: &'a HashMap<String, PrimaryMeta>,
pub sequences: &'a HashMap<u32, SequenceMeta>,
pub filter_role: Option<Role>,
pub customization: Option<&'a HtmlCustomization>,
}
#[cfg(feature = "serde")]
pub(super) fn generate_html(config: HtmlConfig) -> std::io::Result<()> {
let HtmlConfig {
html_path,
project_name,
version,
errors,
components,
primaries,
sequences,
filter_role,
customization,
} = config;
let role_badge = match filter_role {
Some(Role::Public) => r#"<div class="role-badge role-public">📘 PUBLIC DOCS</div>"#,
Some(Role::Developer) => {
r#"<div class="role-badge role-developer">👨💻 DEVELOPER DOCS</div>"#
}
Some(Role::Internal) => r#"<div class="role-badge role-internal">🔒 INTERNAL DOCS</div>"#,
None => "",
};
let data_json = generate_data_json(
project_name,
version,
errors,
components,
primaries,
sequences,
filter_role,
);
let mut languages = std::collections::HashSet::new();
for error in errors {
for snippet in &error.code_snippets {
if let Some(lang) = &snippet.language {
languages.insert(lang.as_str());
}
}
}
let languages: Vec<&str> = languages.into_iter().collect();
let html = template::generate_template(
role_badge,
project_name,
version,
&data_json,
&languages,
customization,
);
let json_path = Path::new(html_path).with_extension("json");
if let Some(parent) = Path::new(html_path).parent() {
fs::create_dir_all(parent)?;
}
fs::write(html_path, html)?;
fs::write(json_path, data_json)?;
Ok(())
}