/* ============================================
STATE MANAGEMENT
Global State, Constants, and DOM References
============================================ */
// === Application State ===
const AppState = {
// Data
errors: [],
filteredErrors: [],
// Filters
searchQuery: '',
activeSeverities: new Set(['critical', 'error', 'warning', 'info', 'debug', 'trace']),
activeCategory: null,
activeTag: null,
// Visibility
documentRole: 'public', // Set by server: 'internal', 'developer', 'public'
viewingAs: null, // null = native role, or 'developer'/'public' for restricted view
// UI State
selectedErrorCode: null,
expandedCards: new Set(),
openSections: new Map(), // Map of errorCode -> Set of open section IDs
// Sidebar
activeSidebarTab: 'browse', // 'browse' or 'detail'
expandedTreeNodes: new Set(),
};
// === Severity Order (WDP Part 10 - matches Rust Severity enum) ===
const SEVERITY_ORDER = ['error', 'blocked', 'critical', 'warning', 'help', 'success', 'completed', 'info', 'trace'];
// === Severity Character Codes (WDP Part 10) ===
const SEVERITY_CHARS = {
error: 'E',
blocked: 'B',
critical: 'C',
warning: 'W',
help: 'H',
success: 'S',
completed: 'K', // K for Kompleted (C is taken by Critical)
info: 'I',
trace: 'T'
};
// === Severity Icons (using HTML entities for encoding safety) ===
const SEVERITY_ICONS = {
error: '\u274C',
blocked: '\u26D4',
critical: '\uD83D\uDD25',
warning: '\u26A0\uFE0F',
help: '\uD83D\uDCA1',
success: '\u2705',
completed: '\u2714\uFE0F',
info: '\u2139\uFE0F',
trace: '\uD83D\uDD0D'
};
// === Visibility Levels ===
const VISIBILITY_LEVELS = {
internal: 0,
developer: 1,
public: 2
};
// === DOM Element References ===
const DOM = {};
// Initialize DOM references after document load
function initDOMReferences() {
// Search elements (matches template.rs IDs)
DOM.searchInput = document.getElementById('searchInput');
DOM.searchClear = document.getElementById('searchClear');
DOM.queryBuilderToggle = document.getElementById('queryBuilderToggle');
DOM.queryBuilder = document.getElementById('queryBuilderDropdown');
DOM.autocompleteDropdown = document.getElementById('autocompleteDropdown');
// Filter bars
DOM.severityBar = document.getElementById('severityFilterBar');
DOM.visibilityRow = document.getElementById('visibilityRow');
// Results
DOM.resultsContainer = document.getElementById('results');
DOM.resultsCount = document.getElementById('resultCount');
// Sidebar
DOM.browseList = document.getElementById('browseList');
DOM.detailPanel = document.getElementById('detailPanel');
DOM.detailContent = document.getElementById('detailContent');
// Theme
DOM.themeToggle = document.getElementById('themeToggle');
DOM.formatToggle = document.getElementById('formatToggle');
// Toast
DOM.toastContainer = document.getElementById('toastContainer');
// Query builder parts
DOM.builderPreview = document.getElementById('builderPreview');
DOM.builderCloseBtn = document.getElementById('builderCloseBtn');
DOM.builderClearBtn = document.getElementById('builderClearBtn');
DOM.builderSearchBtn = document.getElementById('builderSearchBtn');
}
// === Utility: Get current effective visibility ===
function getEffectiveVisibility() {
return AppState.viewingAs || AppState.documentRole;
}
// === Utility: Check if error is visible at current level ===
function isErrorVisible(error) {
const effectiveLevel = VISIBILITY_LEVELS[getEffectiveVisibility()];
const errorLevel = VISIBILITY_LEVELS[error.visibility || 'public'];
return errorLevel >= effectiveLevel;
}
/* ============================================
UTILITY FUNCTIONS
Helper functions used across modules
============================================ */
// === Debounce ===
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// === Throttle ===
function throttle(fn, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// === Escape HTML ===
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// === Copy to Clipboard ===
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
if (button) {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 2000);
}
showToast('Copied!', 'Text copied to clipboard', 'success');
return true;
} catch (err) {
console.error('Failed to copy:', err);
showToast('Copy Failed', 'Unable to copy to clipboard', 'error');
return false;
}
}
// === Create Element Helper ===
function createElement(tag, className, attributes = {}) {
const el = document.createElement(tag);
if (className) el.className = className;
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'textContent') {
el.textContent = value;
} else if (key === 'innerHTML') {
el.innerHTML = value;
} else if (key.startsWith('data-')) {
el.setAttribute(key, value);
} else {
el[key] = value;
}
});
return el;
}
// === Format Code for Display ===
function formatCode(code, language = 'rust') {
// Basic syntax highlighting (Prism handles the rest)
return code;
}
// === Highlight Search Match ===
function highlightMatch(text, query) {
if (!query || !text) return escapeHtml(text);
const escaped = escapeHtml(text);
const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi');
return escaped.replace(regex, '<mark>$1</mark>');
}
// === Escape RegExp ===
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// === Plural Helper ===
function plural(count, singular, pluralForm) {
return count === 1 ? singular : (pluralForm || singular + 's');
}
// === Generate Unique ID ===
function generateId(prefix = 'id') {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// === Get Category from Code ===
function getCategoryFromCode(code) {
if (!code) return 'Unknown';
// Extract category prefix (e.g., "E" from "E0001", "AUTH" from "AUTH_001")
const match = code.match(/^([A-Z]+)/);
return match ? match[1] : 'Other';
}
// === Parse Error Code Parts ===
function parseErrorCode(code) {
if (!code) return { prefix: '', number: '' };
const match = code.match(/^([A-Z_]+)(\d+)$/);
if (match) {
return { prefix: match[1], number: match[2] };
}
return { prefix: code, number: '' };
}
// === Scroll to Element ===
function scrollToElement(element, options = {}) {
const defaults = {
behavior: 'smooth',
block: 'center',
inline: 'nearest'
};
element.scrollIntoView({ ...defaults, ...options });
}
// === Local Storage Helpers ===
const Storage = {
get(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
},
remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch {
return false;
}
}
};
/* ============================================
TOAST NOTIFICATIONS
Show feedback messages to user
============================================ */
function showToast(title, message = '', type = 'info', duration = 3000) {
const container = DOM.toastContainer;
if (!container) return;
const toast = createElement('div', `toast ${type}`);
const icons = {
success: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
warning: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
};
toast.innerHTML = `
<span class="toast-icon">${icons[type] || icons.info}</span>
<div class="toast-content">
<div class="toast-title">${escapeHtml(title)}</div>
${message ? `<div class="toast-message">${escapeHtml(message)}</div>` : ''}
</div>
<button class="toast-close" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
`;
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => dismissToast(toast));
container.appendChild(toast);
if (duration > 0) {
setTimeout(() => dismissToast(toast), duration);
}
return toast;
}
function dismissToast(toast) {
if (!toast || toast.classList.contains('hiding')) return;
toast.classList.add('hiding');
toast.addEventListener('animationend', () => {
toast.remove();
});
}
/* ============================================
THEME MANAGEMENT
Dark/Light theme toggle with persistence
============================================ */
function initTheme() {
// Check saved preference or system preference
const saved = Storage.get('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
setTheme(theme);
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!Storage.get('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
updateThemeToggleIcon(theme);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
setTheme(next);
Storage.set('theme', next);
}
function updateThemeToggleIcon(theme) {
const toggle = DOM.themeToggle;
if (!toggle) return;
const sunIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
const moonIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
toggle.innerHTML = theme === 'dark' ? sunIcon : moonIcon;
toggle.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`);
toggle.setAttribute('title', `Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`);
}
/* ============================================
VISIBILITY MANAGEMENT
Role-based visibility switching
============================================ */
function initVisibility() {
// Get document role from data attribute or default
const roleEl = document.querySelector('.role-badge');
const docRole = roleEl?.textContent?.toLowerCase()?.trim() || 'public';
// Map role text to canonical names
const roleMap = {
'internal': 'internal',
'internal only': 'internal',
'developer': 'developer',
'dev': 'developer',
'public': 'public',
'pub': 'public'
};
AppState.documentRole = roleMap[docRole] || 'public';
AppState.viewingAs = null;
// Don't show visibility row for public docs
if (AppState.documentRole === 'public') {
if (DOM.visibilityRow) {
DOM.visibilityRow.style.display = 'none';
}
return;
}
// Build visibility switcher
buildVisibilitySwitcher();
updateVisibilityDisplay();
}
function buildVisibilitySwitcher() {
if (!DOM.visibilityRow) return;
const docRole = AppState.documentRole;
const options = getAvailableVisibilityOptions(docRole);
DOM.visibilityRow.innerHTML = `
<span class="visibility-label">VIEW AS</span>
<div class="visibility-switcher" id="visibilitySwitcher">
<button class="visibility-current ${docRole}" aria-haspopup="listbox" aria-expanded="false">
<span class="vis-dot"></span>
<span class="vis-label">${capitalize(docRole)}</span>
</button>
<div class="visibility-dropdown" role="listbox">
${options.map(opt => `
<button class="visibility-option ${opt}${opt === docRole ? ' selected' : ''}"
data-visibility="${opt}" role="option">
<span class="vis-dot"></span>
<span>${capitalize(opt)}</span>
</button>
`).join('')}
</div>
</div>
<span class="viewing-counter" id="viewingCounter"></span>
<button class="visibility-reset" id="visibilityReset" style="display: none;">Reset</button>
`;
// Store references
const switcher = DOM.visibilityRow.querySelector('.visibility-switcher');
const currentBtn = switcher.querySelector('.visibility-current');
// Toggle dropdown
currentBtn.addEventListener('click', (e) => {
e.stopPropagation();
switcher.classList.toggle('open');
currentBtn.setAttribute('aria-expanded', switcher.classList.contains('open'));
});
// Option clicks
switcher.querySelectorAll('.visibility-option').forEach(opt => {
opt.addEventListener('click', () => selectVisibility(opt.dataset.visibility));
});
// Reset button
const resetBtn = DOM.visibilityRow.querySelector('.visibility-reset');
if (resetBtn) {
resetBtn.addEventListener('click', resetVisibility);
}
// Close dropdown on outside click
document.addEventListener('click', (e) => {
if (!switcher.contains(e.target)) {
switcher.classList.remove('open');
currentBtn.setAttribute('aria-expanded', 'false');
}
});
}
function getAvailableVisibilityOptions(role) {
switch (role) {
case 'internal':
return ['internal', 'developer', 'public'];
case 'developer':
return ['developer', 'public'];
case 'public':
return ['public'];
default:
return ['public'];
}
}
function selectVisibility(visibility) {
const docRole = AppState.documentRole;
// Set viewingAs (null if same as native role)
AppState.viewingAs = (visibility === docRole) ? null : visibility;
// Close dropdown
const switcher = DOM.visibilityRow?.querySelector('.visibility-switcher');
if (switcher) {
switcher.classList.remove('open');
}
// Update display
updateVisibilityDisplay();
// Re-filter and render
filterErrors();
renderResults();
updateSidebar();
}
function updateVisibilityDisplay() {
if (!DOM.visibilityRow) return;
const switcher = DOM.visibilityRow.querySelector('.visibility-switcher');
if (!switcher) return;
const currentBtn = switcher.querySelector('.visibility-current');
const effective = getEffectiveVisibility();
// Update button class and label
currentBtn.className = `visibility-current ${effective}`;
currentBtn.innerHTML = `
<span class="vis-dot"></span>
<span class="vis-label">${capitalize(effective)}</span>
`;
// Update selected option
switcher.querySelectorAll('.visibility-option').forEach(opt => {
opt.classList.toggle('selected', opt.dataset.visibility === effective);
});
// Update viewing counter
updateViewingCounter();
// Show/hide reset button
const resetBtn = DOM.visibilityRow.querySelector('.visibility-reset');
if (resetBtn) {
resetBtn.style.display = AppState.viewingAs ? 'inline-flex' : 'none';
}
}
function updateViewingCounter() {
const counter = document.getElementById('viewingCounter');
if (!counter) return;
const visible = AppState.filteredErrors.length;
const total = AppState.errors.length;
counter.innerHTML = `Viewing <strong>${visible}</strong> of <strong>${total}</strong> ${plural(total, 'error')}`;
}
function resetVisibility() {
AppState.viewingAs = null;
updateVisibilityDisplay();
filterErrors();
renderResults();
updateSidebar();
}
function capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
/* ============================================
SEARCH & FILTERING
Search input handling and error filtering
============================================ */
function initSearch() {
if (!DOM.searchInput) return;
// Search input with debounce
DOM.searchInput.addEventListener('input', debounce((e) => {
AppState.searchQuery = e.target.value.trim();
filterErrors();
renderResults();
updateSidebar();
}, 200));
// Keyboard shortcut (Ctrl/Cmd + K)
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
DOM.searchInput.focus();
DOM.searchInput.select();
}
// Escape to clear
if (e.key === 'Escape' && document.activeElement === DOM.searchInput) {
DOM.searchInput.blur();
}
});
}
function initSeverityFilters() {
if (!DOM.severityBar) return;
// Initialize all severities as active
AppState.activeSeverities = new Set(SEVERITY_ORDER);
// Count errors by severity
const counts = {};
SEVERITY_ORDER.forEach(sev => counts[sev] = 0);
AppState.errors.forEach(err => {
const sev = (err.severity || 'info').toLowerCase();
if (counts.hasOwnProperty(sev)) counts[sev]++;
});
// Build severity pills using data from the global severities object if available
DOM.severityBar.innerHTML = '<span class="filter-label">SEVERITY</span>';
SEVERITY_ORDER.forEach(severity => {
const count = counts[severity];
if (count === 0) return; // Skip severities with no errors
// Get severity character code from WDP mapping
const sevChar = SEVERITY_CHARS[severity] || severity.charAt(0).toUpperCase();
const sevData = AppState.severities?.[sevChar] || {};
const btn = createElement('button', `severity-filter-item severity-${severity} active`, {
'data-severity': severity,
'aria-pressed': 'true',
'title': `${capitalize(severity)}: ${count} error${count !== 1 ? 's' : ''}`
});
const emoji = sevData.emoji || SEVERITY_ICONS[severity] || '';
btn.innerHTML = `
<span class="sev-emoji">${emoji}</span>
<span class="sev-char">${sevChar}</span>
<span class="sev-count">${count}</span>
`;
btn.addEventListener('click', () => toggleSeverity(severity, btn));
DOM.severityBar.appendChild(btn);
});
}
function toggleSeverity(severity, btn) {
if (AppState.activeSeverities.has(severity)) {
// Don't allow deselecting all
if (AppState.activeSeverities.size === 1) {
showToast('Cannot Remove', 'At least one severity must be selected', 'warning');
return;
}
AppState.activeSeverities.delete(severity);
btn.classList.remove('active');
btn.setAttribute('aria-pressed', 'false');
} else {
AppState.activeSeverities.add(severity);
btn.classList.add('active');
btn.setAttribute('aria-pressed', 'true');
}
filterErrors();
renderResults();
updateSidebar();
}
function filterErrors() {
const query = AppState.searchQuery.toLowerCase();
AppState.filteredErrors = AppState.errors.filter(error => {
// Visibility filter
if (!isErrorVisible(error)) return false;
// Severity filter
const severity = (error.severity || 'info').toLowerCase();
if (!AppState.activeSeverities.has(severity)) return false;
// Category filter (component in this system)
if (AppState.activeCategory) {
const component = error.component || getCategoryFromCode(error.code);
if (component !== AppState.activeCategory) return false;
}
// Tag filter
if (AppState.activeTag) {
const tags = error.tags || [];
if (!tags.includes(AppState.activeTag)) return false;
}
// Search query - support wildcard pattern matching
if (query) {
// Check if it's a wildcard pattern like *.*.*.* or E.*.*.*
if (query.includes('*') || query.includes('.')) {
if (!matchWildcardPattern(error.code, query)) return false;
} else {
// Regular text search
const searchFields = [
error.code,
error.message,
error.description,
...(error.tags || []),
...(error.hints || []).map(h => typeof h === 'string' ? h : h.message || '')
].filter(Boolean).join(' ').toLowerCase();
if (!searchFields.includes(query)) return false;
}
}
return true;
});
// Update viewing counter
updateViewingCounter();
}
function matchWildcardPattern(code, pattern) {
if (!code) return false;
// Split both by dots
const codeParts = code.split('.');
const patternParts = pattern.split('.');
// Must have same number of parts for exact wildcard matching
if (codeParts.length !== patternParts.length && !pattern.includes('*')) {
// Fallback to substring match
return code.toLowerCase().includes(pattern.toLowerCase());
}
// Check each part
for (let i = 0; i < patternParts.length; i++) {
const pp = patternParts[i];
const cp = codeParts[i] || '';
if (pp === '*') continue; // Wildcard matches anything
if (pp.toLowerCase() !== cp.toLowerCase()) return false;
}
return true;
}
function setCategory(category) {
AppState.activeCategory = category;
filterErrors();
renderResults();
updateSidebar();
}
function setTag(tag) {
AppState.activeTag = tag;
filterErrors();
renderResults();
updateSidebar();
}
function clearFilters() {
AppState.searchQuery = '';
AppState.activeCategory = null;
AppState.activeTag = null;
AppState.activeSeverities = new Set(SEVERITY_ORDER);
if (DOM.searchInput) DOM.searchInput.value = '*.*.*.*';
// Reset severity pills
document.querySelectorAll('.severity-pill').forEach(pill => {
pill.classList.add('active');
pill.setAttribute('aria-pressed', 'true');
});
filterErrors();
renderResults();
updateSidebar();
}
/* ============================================
ERROR CARD RENDERING
Render error cards with collapsible sections
============================================ */
function renderResults() {
if (!DOM.resultsContainer) return;
const errors = AppState.filteredErrors;
// Update count
if (DOM.resultsCount) {
DOM.resultsCount.textContent = `${errors.length} ${plural(errors.length, 'error')}`;
}
if (errors.length === 0) {
DOM.resultsContainer.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<h3>No errors found</h3>
<p>Try adjusting your search or filter criteria</p>
</div>
`;
return;
}
// Render cards
DOM.resultsContainer.innerHTML = '';
errors.forEach(error => {
const card = renderErrorCard(error);
DOM.resultsContainer.appendChild(card);
});
// Apply Prism highlighting
if (typeof Prism !== 'undefined') {
Prism.highlightAllUnder(DOM.resultsContainer);
}
}
function renderErrorCard(error) {
const severity = (error.severity || 'info').toLowerCase();
const isExpanded = AppState.expandedCards.has(error.code);
const card = createElement('article', `error-card${isExpanded ? ' expanded' : ''}`, {
'data-code': error.code,
'data-severity': severity
});
// Header
const header = renderCardHeader(error, severity);
// Body (collapsible sections)
const body = renderCardBody(error);
card.appendChild(header);
card.appendChild(body);
return card;
}
function renderCardHeader(error, severity) {
const header = createElement('div', 'error-header');
// Severity indicator
const indicator = createElement('div', `severity-indicator ${severity}`, {
title: capitalize(severity)
});
indicator.textContent = SEVERITY_ICONS[severity];
// Error info
const info = createElement('div', 'error-info');
// Title row with code, hash, and copy button
const titleRow = createElement('div', 'error-title-row');
const codeEl = createElement('span', 'error-code', {
textContent: error.code
});
const hashEl = error.hash ? createElement('span', 'error-hash', {
textContent: error.hash,
title: 'Error hash for cross-referencing'
}) : null;
const copyBtn = createElement('button', 'copy-btn', {
title: 'Copy error code',
'aria-label': 'Copy error code'
});
copyBtn.innerHTML = '\uD83D\uDCCB';
copyBtn.addEventListener('click', (e) => {
e.stopPropagation();
copyToClipboard(error.code, copyBtn);
});
titleRow.appendChild(codeEl);
if (hashEl) titleRow.appendChild(hashEl);
titleRow.appendChild(copyBtn);
// Message
const message = createElement('p', 'error-message');
message.innerHTML = highlightMatch(error.message, AppState.searchQuery);
// Meta info
const meta = createElement('div', 'error-meta');
// Visibility badge
if (error.visibility && error.visibility !== 'public') {
const visBadge = createElement('span', `meta-item visibility-${error.visibility}`, {
textContent: capitalize(error.visibility)
});
meta.appendChild(visBadge);
}
// Category
const category = getCategoryFromCode(error.code);
const catBadge = createElement('span', 'meta-item', {
textContent: category
});
meta.appendChild(catBadge);
info.appendChild(titleRow);
info.appendChild(message);
info.appendChild(meta);
// Expand icon
const expandIcon = createElement('span', 'expand-icon');
expandIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>';
header.appendChild(indicator);
header.appendChild(info);
header.appendChild(expandIcon);
// Click to expand/collapse
header.addEventListener('click', () => {
toggleCard(error.code);
});
return header;
}
function renderCardBody(error) {
const body = createElement('div', 'error-body');
// Determine which sections exist
const sections = [];
// CODE section (if there's a code example)
if (error.code_example || error.example) {
sections.push({
id: 'code',
title: 'CODE',
icon: '💻',
render: () => renderCodeSection(error)
});
}
// HINTS section
if (error.hints && error.hints.length > 0) {
sections.push({
id: 'hints',
title: 'HINTS',
icon: '\uD83D\uDCA1',
count: error.hints.length,
render: () => renderHintsSection(error)
});
}
// RELATED section
if (error.related && error.related.length > 0) {
sections.push({
id: 'related',
title: 'RELATED',
icon: '🔗',
count: error.related.length,
render: () => renderRelatedSection(error)
});
}
// TAGS section
if (error.tags && error.tags.length > 0) {
sections.push({
id: 'tags',
title: 'TAGS',
icon: '🏷️',
count: error.tags.length,
render: () => renderTagsSection(error)
});
}
// If no sections, show description
if (sections.length === 0 && error.description) {
const descSection = createElement('div', 'section-content');
descSection.innerHTML = `<p class="detail-message">${escapeHtml(error.description)}</p>`;
body.appendChild(descSection);
return body;
}
// Get open sections for this error
const openSections = AppState.openSections.get(error.code) || new Set([sections[0]?.id]);
sections.forEach(section => {
const isOpen = openSections.has(section.id);
const sectionEl = createElement('div', `section ${section.id}-section${isOpen ? ' open' : ''}`);
sectionEl.dataset.section = section.id;
// Section header
const sectionHeader = createElement('div', 'section-header');
sectionHeader.innerHTML = `
<span class="section-toggle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
<span class="section-icon">${section.icon}</span>
<span class="section-title">${section.title}</span>
${section.count ? `<span class="section-count">${section.count}</span>` : ''}
`;
sectionHeader.addEventListener('click', () => {
toggleSection(error.code, section.id, sectionEl);
});
// Section content
const content = createElement('div', 'section-content');
content.appendChild(section.render());
sectionEl.appendChild(sectionHeader);
sectionEl.appendChild(content);
body.appendChild(sectionEl);
});
return body;
}
function renderCodeSection(error) {
const codeBlock = createElement('div', 'code-block');
const code = error.code_example || error.example || '';
const lang = error.language || 'rust';
codeBlock.innerHTML = `
<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>
<button class="code-copy-btn" title="Copy code">Copy</button>
`;
const copyBtn = codeBlock.querySelector('.code-copy-btn');
copyBtn.addEventListener('click', () => copyToClipboard(code, copyBtn));
return codeBlock;
}
function renderHintsSection(error) {
const list = createElement('div', 'hints-list');
(error.hints || []).forEach((hint, index) => {
const hintText = typeof hint === 'string' ? hint : hint.message;
const hintCode = typeof hint === 'object' ? hint.code : null;
const item = createElement('div', 'hint-item');
item.innerHTML = `
<span class="hint-icon">💡</span>
<div class="hint-content">
<p class="hint-text">${escapeHtml(hintText)}</p>
${hintCode ? `<pre class="hint-code"><code>${escapeHtml(hintCode)}</code></pre>` : ''}
</div>
`;
list.appendChild(item);
});
return list;
}
function renderRelatedSection(error) {
const list = createElement('div', 'related-list');
(error.related || []).forEach(related => {
const relatedCode = typeof related === 'string' ? related : related.code;
const relatedMsg = typeof related === 'object' ? related.message : '';
const item = createElement('button', 'related-item');
item.innerHTML = `
<span class="related-code">${escapeHtml(relatedCode)}</span>
<span class="related-message">${escapeHtml(relatedMsg)}</span>
<span class="related-arrow">→</span>
`;
item.addEventListener('click', () => {
// Find and scroll to related error
const targetCard = document.querySelector(`.error-card[data-code="${relatedCode}"]`);
if (targetCard) {
scrollToElement(targetCard);
targetCard.classList.add('highlight');
setTimeout(() => targetCard.classList.remove('highlight'), 2000);
} else {
// Clear filters and search for it
DOM.searchInput.value = relatedCode;
AppState.searchQuery = relatedCode;
filterErrors();
renderResults();
}
});
list.appendChild(item);
});
return list;
}
function renderTagsSection(error) {
const list = createElement('div', 'tags-list');
(error.tags || []).forEach(tag => {
const item = createElement('button', 'tag-item');
item.innerHTML = `<span class="tag-icon">#</span>${escapeHtml(tag)}`;
item.addEventListener('click', () => {
setTag(tag);
});
list.appendChild(item);
});
return list;
}
function toggleCard(code) {
const card = document.querySelector(`.error-card[data-code="${code}"]`);
if (!card) return;
if (AppState.expandedCards.has(code)) {
AppState.expandedCards.delete(code);
card.classList.remove('expanded');
} else {
AppState.expandedCards.add(code);
card.classList.add('expanded');
// Initialize open sections if not set
if (!AppState.openSections.has(code)) {
const firstSection = card.querySelector('.section');
if (firstSection) {
AppState.openSections.set(code, new Set([firstSection.dataset.section]));
}
}
}
// Update sidebar if viewing this error
if (AppState.selectedErrorCode === code) {
showErrorDetail(code);
}
}
function toggleSection(errorCode, sectionId, sectionEl) {
const openSections = AppState.openSections.get(errorCode) || new Set();
if (openSections.has(sectionId)) {
openSections.delete(sectionId);
sectionEl.classList.remove('open');
} else {
openSections.add(sectionId);
sectionEl.classList.add('open');
}
AppState.openSections.set(errorCode, openSections);
}
/* ============================================
SIDEBAR MANAGEMENT
Browse tree and detail panel
============================================ */
function initSidebar() {
// Tab switching
document.querySelectorAll('.browse-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
switchBrowseTab(tabId);
});
});
// Note: buildBrowseTree() is called by updateSidebar() after filterErrors()
}
function switchBrowseTab(tabId) {
// Update tab active states
document.querySelectorAll('.browse-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabId);
});
// Build the appropriate list based on tab
buildBrowseTree(tabId);
}
function updateSidebar() {
const activeTab = document.querySelector('.browse-tab.active');
buildBrowseTree(activeTab?.dataset.tab || 'components');
if (AppState.selectedErrorCode) {
showErrorDetail(AppState.selectedErrorCode);
}
}
function buildBrowseTree(tabType = 'components') {
if (!DOM.browseList) return;
let items = [];
switch (tabType) {
case 'components':
items = buildComponentList();
break;
case 'primaries':
items = buildPrimaryList();
break;
case 'sequences':
items = buildSequenceList();
break;
default:
items = buildComponentList();
}
if (items.length === 0) {
DOM.browseList.innerHTML = '<div class="browse-empty">No items to display</div>';
return;
}
DOM.browseList.innerHTML = items.map(item => `
<div class="browse-item ${AppState.activeCategory === item.id ? 'active' : ''}"
data-type="${tabType}" data-id="${item.id}">
<span class="browse-icon">${item.icon || ''}</span>
<span class="browse-name">${escapeHtml(item.name)}</span>
<span class="browse-count">${item.count}</span>
</div>
`).join('');
// Click handlers
DOM.browseList.querySelectorAll('.browse-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
const type = item.dataset.type;
// Toggle active state
if (AppState.activeCategory === id) {
AppState.activeCategory = null;
} else {
AppState.activeCategory = id;
}
// Update visual state
DOM.browseList.querySelectorAll('.browse-item').forEach(i => {
i.classList.toggle('active', i.dataset.id === AppState.activeCategory);
});
filterErrors();
renderResults();
});
});
}
function buildComponentList() {
const components = new Map();
AppState.filteredErrors.forEach(error => {
const comp = error.component || getCategoryFromCode(error.code);
if (!components.has(comp)) {
components.set(comp, { id: comp, name: comp, count: 0, icon: '' });
}
components.get(comp).count++;
});
// Merge with metadata if available
Object.entries(AppState.components || {}).forEach(([id, meta]) => {
if (components.has(id)) {
components.get(id).name = meta.name || id;
components.get(id).icon = meta.emoji || '';
}
});
return Array.from(components.values()).sort((a, b) => b.count - a.count);
}
function buildPrimaryList() {
const primaries = new Map();
AppState.filteredErrors.forEach(error => {
const prim = error.primary;
if (!prim) return;
if (!primaries.has(prim)) {
primaries.set(prim, { id: prim, name: prim, count: 0, icon: '' });
}
primaries.get(prim).count++;
});
// Merge with metadata if available
Object.entries(AppState.primaries || {}).forEach(([id, meta]) => {
if (primaries.has(id)) {
primaries.get(id).name = meta.name || id;
}
});
return Array.from(primaries.values()).sort((a, b) => b.count - a.count);
}
function buildSequenceList() {
const sequences = new Map();
AppState.filteredErrors.forEach(error => {
const seq = error.sequence;
if (!seq) return;
if (!sequences.has(seq)) {
sequences.set(seq, { id: seq, name: seq, count: 0, icon: '' });
}
sequences.get(seq).count++;
});
// Merge with metadata
Object.entries(AppState.sequences || {}).forEach(([id, meta]) => {
if (sequences.has(id)) {
sequences.get(id).name = meta.name || id;
}
});
return Array.from(sequences.values()).sort((a, b) => b.count - a.count);
}
function selectError(code) {
AppState.selectedErrorCode = code;
showErrorDetail(code);
// Scroll to and expand the card
const card = document.querySelector(`.error-card[data-code="${code}"]`);
if (card) {
if (!AppState.expandedCards.has(code)) {
toggleCard(code);
}
scrollToElement(card);
}
}
function showErrorDetail(code) {
if (!DOM.detailContent) return;
const error = AppState.errors.find(e => e.code === code);
if (!error) {
DOM.detailContent.innerHTML = `
<div class="detail-empty">
<span class="detail-empty-icon">📄</span>
<p>Select an error to view details</p>
</div>
`;
return;
}
const severity = error.severity || 'info';
const sevData = AppState.severities?.[severity.charAt(0).toUpperCase()] || {};
DOM.detailContent.innerHTML = `
<div class="detail-header">
<div class="detail-code">${escapeHtml(error.code)}</div>
<span class="detail-severity severity-${severity}">
${sevData.emoji || SEVERITY_ICONS[severity] || ''} ${capitalize(severity)}
</span>
${error.hash ? `<span class="detail-hash">#${escapeHtml(error.hash)}</span>` : ''}
</div>
<div class="detail-section">
<div class="detail-label">Message</div>
<div class="detail-text">${escapeHtml(error.message)}</div>
</div>
${error.description ? `
<div class="detail-section">
<div class="detail-label">Description</div>
<div class="detail-text">${escapeHtml(error.description)}</div>
</div>
` : ''}
${error.hints && error.hints.length ? `
<div class="detail-section">
<div class="detail-label">Hints</div>
<ul class="detail-hints">
${error.hints.map(h => `
<li>${escapeHtml(typeof h === 'string' ? h : h.message || '')}</li>
`).join('')}
</ul>
</div>
` : ''}
${error.related && error.related.length ? `
<div class="detail-section">
<div class="detail-label">Related Errors</div>
<div class="detail-related">
${error.related.map(r => {
const relCode = typeof r === 'string' ? r : r.code;
return `<button class="related-link" onclick="selectError('${relCode}')">${escapeHtml(relCode)}</button>`;
}).join('')}
</div>
</div>
` : ''}
<div class="detail-actions">
<button class="detail-btn" onclick="copyToClipboard('${error.code}', this)">
📋 Copy Code
</button>
<button class="detail-btn" onclick="scrollToElement(document.querySelector('.error-card[data-code=\\'${error.code}\\']'))">
🔍 Find in List
</button>
</div>
`;
}
/* ============================================
QUERY BUILDER
Visual query construction interface
============================================ */
function initQueryBuilder() {
if (!DOM.queryBuilderToggle || !DOM.queryBuilder) return;
// Toggle button
DOM.queryBuilderToggle.addEventListener('click', () => {
DOM.queryBuilder.classList.toggle('open');
DOM.queryBuilderToggle.classList.toggle('active');
});
// Close button
if (DOM.builderCloseBtn) {
DOM.builderCloseBtn.addEventListener('click', () => {
DOM.queryBuilder.classList.remove('open');
DOM.queryBuilderToggle.classList.remove('active');
});
}
// Clear button
if (DOM.builderClearBtn) {
DOM.builderClearBtn.addEventListener('click', clearQueryBuilder);
}
// Search button
if (DOM.builderSearchBtn) {
DOM.builderSearchBtn.addEventListener('click', applyQueryBuilder);
}
// Build column options
buildQueryColumns();
// Tab switching for mobile
document.querySelectorAll('.builder-tab').forEach(tab => {
tab.addEventListener('click', () => {
const col = tab.dataset.col;
// Update tabs
document.querySelectorAll('.builder-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update columns
document.querySelectorAll('.builder-column').forEach(c => {
c.classList.toggle('active', c.dataset.col === col);
});
});
});
}
function buildQueryColumns() {
// Severity column
const sevCol = document.getElementById('sevCol');
if (sevCol) {
sevCol.innerHTML = `
<div class="column-option selected" data-value="*">
<span class="option-indicator"></span>
<span class="option-label">All Severities</span>
</div>
${Object.entries(AppState.severities || {}).map(([char, data]) => `
<div class="column-option" data-value="${char}">
<span class="option-indicator"></span>
<span class="option-emoji">${data.emoji || ''}</span>
<span class="option-label">${data.name || char}</span>
</div>
`).join('')}
`;
setupColumnOptions(sevCol, 'sevCurrent');
}
// Component column
const compCol = document.getElementById('compCol');
if (compCol) {
const components = Object.entries(AppState.components || {});
compCol.innerHTML = `
<div class="column-option selected" data-value="*">
<span class="option-indicator"></span>
<span class="option-label">All Components</span>
</div>
${components.map(([id, data]) => `
<div class="column-option" data-value="${id}">
<span class="option-indicator"></span>
<span class="option-emoji">${data.emoji || ''}</span>
<span class="option-label">${data.name || id}</span>
</div>
`).join('')}
`;
setupColumnOptions(compCol, 'compCurrent');
}
// Primary column
const primCol = document.getElementById('primCol');
if (primCol) {
const primaries = Object.entries(AppState.primaries || {});
primCol.innerHTML = `
<div class="column-option selected" data-value="*">
<span class="option-indicator"></span>
<span class="option-label">All Primaries</span>
</div>
${primaries.map(([id, data]) => `
<div class="column-option" data-value="${id}">
<span class="option-indicator"></span>
<span class="option-label">${data.name || id}</span>
</div>
`).join('')}
`;
setupColumnOptions(primCol, 'primCurrent');
}
// Sequence column
const seqCol = document.getElementById('seqCol');
if (seqCol) {
const sequences = Object.entries(AppState.sequences || {});
seqCol.innerHTML = `
<div class="column-option selected" data-value="*">
<span class="option-indicator"></span>
<span class="option-label">All Sequences</span>
</div>
${sequences.map(([id, data]) => `
<div class="column-option" data-value="${id}">
<span class="option-indicator"></span>
<span class="option-label">${data.name || id}</span>
</div>
`).join('')}
`;
setupColumnOptions(seqCol, 'seqCurrent');
}
updateBuilderPreview();
}
function setupColumnOptions(column, currentId) {
const currentEl = document.getElementById(currentId);
column.querySelectorAll('.column-option').forEach(opt => {
opt.addEventListener('click', () => {
// Update selection
column.querySelectorAll('.column-option').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
// Update current display
if (currentEl) {
const value = opt.dataset.value;
currentEl.textContent = `[${value}]`;
}
updateBuilderPreview();
});
});
}
function updateBuilderPreview() {
if (!DOM.builderPreview) return;
const sev = document.querySelector('#sevCol .column-option.selected')?.dataset.value || '*';
const comp = document.querySelector('#compCol .column-option.selected')?.dataset.value || '*';
const prim = document.querySelector('#primCol .column-option.selected')?.dataset.value || '*';
const seq = document.querySelector('#seqCol .column-option.selected')?.dataset.value || '*';
DOM.builderPreview.textContent = `${sev}.${comp}.${prim}.${seq}`;
}
function applyQueryBuilder() {
const pattern = DOM.builderPreview?.textContent || '*.*.*.*';
// Set search query
if (DOM.searchInput) {
DOM.searchInput.value = pattern;
}
AppState.searchQuery = pattern;
// Close builder
DOM.queryBuilder.classList.remove('open');
DOM.queryBuilderToggle.classList.remove('active');
// Apply filters
filterErrors();
renderResults();
updateSidebar();
}
function clearQueryBuilder() {
// Reset all columns to wildcard
document.querySelectorAll('.column-option').forEach(opt => {
opt.classList.toggle('selected', opt.dataset.value === '*');
});
// Reset current displays
['sevCurrent', 'compCurrent', 'primCurrent', 'seqCurrent'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '[*]';
});
updateBuilderPreview();
}
/* ============================================
APPLICATION INITIALIZATION
Main entry point and setup
============================================ */
// Initialize the application when DOM is ready
document.addEventListener('DOMContentLoaded', initApp);
function initApp() {
console.log('[WaddlingErrors] Initializing documentation viewer...');
// Initialize DOM references
initDOMReferences();
// Load error data from the global DATA object (set by template.rs)
loadErrorData();
// Initialize modules
initTheme();
initVisibility();
initSearch();
initSeverityFilters();
initQueryBuilder();
initSidebar();
// Initial render
filterErrors();
renderResults();
updateSidebar();
// Theme toggle
if (DOM.themeToggle) {
DOM.themeToggle.addEventListener('click', toggleTheme);
}
console.log(`[WaddlingErrors] Loaded ${AppState.errors.length} errors, ${AppState.filteredErrors.length} visible.`);
}
function loadErrorData() {
// Error data is set by template.rs as global DATA object
if (typeof DATA !== 'undefined') {
AppState.errors = (DATA.errors || []).map(normalizeError);
// Also store component/primary/sequence metadata if available
AppState.components = DATA.components || {};
AppState.primaries = DATA.primaries || {};
AppState.sequences = DATA.sequences || {};
AppState.severities = DATA.severities || {};
} else if (typeof errorDatabase !== 'undefined') {
// Fallback to older variable names
AppState.errors = errorDatabase.map(normalizeError);
} else {
console.warn('[WaddlingErrors] No error data found');
AppState.errors = [];
}
}
function normalizeError(error) {
// Get severity char from code (first character like 'E', 'W', 'C')
const sevChar = error.code?.charAt(0)?.toUpperCase() || 'I';
const severityMap = {
'E': 'error',
'B': 'blocked',
'C': 'critical',
'W': 'warning',
'H': 'help',
'S': 'success',
'K': 'completed',
'I': 'info',
'T': 'trace'
};
// Spread original error first, then override with normalized values
return {
...error,
code: error.code || 'UNKNOWN',
message: error.message || error.msg || '',
description: error.description || error.desc || '',
severity: severityMap[sevChar] || 'info',
visibility: (error.visibility || 'public').toLowerCase(),
hash: error.hash || null,
hints: Array.isArray(error.hints) ? error.hints : [],
related: Array.isArray(error.related) || Array.isArray(error.see_also)
? (error.related || error.see_also)
: [],
tags: Array.isArray(error.tags) ? error.tags : [],
code_example: error.code_example || error.snippet || null,
language: error.language || 'rust',
component: error.component || null,
primary: error.primary || null,
sequence: error.sequence || null,
};
}
// Export functions that might be needed globally
window.WaddlingErrors = {
filterErrors,
renderResults,
toggleCard,
copyToClipboard,
showToast,
setCategory,
setTag,
clearFilters,
selectError,
getState: () => AppState
};