use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::path::PathBuf;
pub const X86_DOC_DEFAULT_CSS: &str = r#"
/* ── X86 Documentation Stylesheet ────────────────────────────────────── */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #495057;
--text-tertiary: #6c757d;
--border-color: #dee2e6;
--link-color: #0d6efd;
--link-hover: #0a58ca;
--code-bg: #f1f3f5;
--code-border: #d0d4d8;
--table-stripe: #f2f4f6;
--warning-bg: #fff3cd;
--warning-border: #ffc107;
--note-bg: #d1e7ff;
--note-border: #0d6efd;
--deprecated-bg: #f8d7da;
--deprecated-border: #dc3545;
--header-bg: #343a40;
--header-text: #f8f9fa;
--font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--sidebar-width: 300px;
--content-max-width: 1100px;
--transition-speed: 0.2s;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1d21;
--bg-secondary: #212529;
--bg-tertiary: #343a40;
--text-primary: #e9ecef;
--text-secondary: #ced4da;
--text-tertiary: #adb5bd;
--border-color: #495057;
--link-color: #6ea8fe;
--link-hover: #8bb9fe;
--code-bg: #2d3136;
--code-border: #495057;
--table-stripe: #2a2d31;
--warning-bg: #4d3a1a;
--warning-border: #997404;
--note-bg: #1a3a5c;
--note-border: #0d6efd;
--deprecated-bg: #4d1a20;
--deprecated-border: #dc3545;
--header-bg: #16191c;
--header-text: #e9ecef;
}
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
display: flex;
min-height: 100vh;
}
/* ── Sidebar ─────────────────────────────────────────────────────── */
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
overflow-y: auto;
padding: 1.5rem 1rem;
position: sticky;
top: 0;
height: 100vh;
font-size: 0.875rem;
}
.sidebar h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-tertiary);
margin: 1.5rem 0 0.5rem;
}
.sidebar h3:first-child { margin-top: 0; }
.sidebar ul {
list-style: none;
padding-left: 0;
}
.sidebar li {
padding: 0.25rem 0;
}
.sidebar a {
color: var(--text-secondary);
text-decoration: none;
display: block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
transition: background var(--transition-speed);
}
.sidebar a:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.sidebar .nested {
padding-left: 1rem;
}
/* ── Search ───────────────────────────────────────────────────────── */
.search-container {
margin-bottom: 1rem;
}
.search-container input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
transition: border-color var(--transition-speed);
}
.search-container input:focus {
outline: none;
border-color: var(--link-color);
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
}
.search-results {
margin-top: 0.5rem;
max-height: 300px;
overflow-y: auto;
}
.search-result-item {
padding: 0.35rem 0.5rem;
cursor: pointer;
border-radius: 4px;
font-size: 0.8125rem;
}
.search-result-item:hover {
background: var(--bg-tertiary);
}
.search-result-item .kind {
color: var(--text-tertiary);
font-size: 0.6875rem;
margin-left: 0.5rem;
}
.search-no-results {
color: var(--text-tertiary);
font-size: 0.8125rem;
font-style: italic;
padding: 0.5rem;
}
/* ── Main Content ─────────────────────────────────────────────────── */
.main-content {
flex: 1;
max-width: var(--content-max-width);
padding: 2rem 3rem;
overflow-y: auto;
}
/* ── Header ───────────────────────────────────────────────────────── */
.doc-header {
background: var(--header-bg);
color: var(--header-text);
padding: 1rem 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.doc-header h1 {
font-size: 1.75rem;
margin: 0;
}
.doc-header .subtitle {
font-size: 0.875rem;
color: var(--text-tertiary);
opacity: 0.8;
}
/* ── Typography ───────────────────────────────────────────────────── */
h1 { font-size: 2rem; margin: 1.5rem 0 1rem; }
h2 { font-size: 1.5rem; margin: 1.5rem 0 0.75rem; border-bottom: 2px solid var(--border-color); padding-bottom: 0.25rem; }
h3 { font-size: 1.25rem; margin: 1.25rem 0 0.5rem; }
h4 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }
p { margin: 0.75rem 0; }
a {
color: var(--link-color);
text-decoration: none;
transition: color var(--transition-speed);
}
a:hover { color: var(--link-hover); text-decoration: underline; }
code {
font-family: var(--font-mono);
background: var(--code-bg);
border: 1px solid var(--code-border);
padding: 0.1rem 0.3rem;
border-radius: 4px;
font-size: 0.875em;
}
pre {
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
}
pre code {
background: none;
border: none;
padding: 0;
}
/* ── Tables ───────────────────────────────────────────────────────── */
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
th, td {
padding: 0.5rem 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: var(--bg-secondary);
font-weight: 600;
font-size: 0.8125rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-tertiary);
}
tr:nth-child(even) td {
background: var(--table-stripe);
}
/* ── Admonitions ──────────────────────────────────────────────────── */
.admonition {
border-left: 4px solid;
padding: 0.75rem 1rem;
margin: 1rem 0;
border-radius: 0 6px 6px 0;
font-size: 0.9375rem;
}
.admonition-note {
background: var(--note-bg);
border-color: var(--note-border);
}
.admonition-warning {
background: var(--warning-bg);
border-color: var(--warning-border);
}
.admonition-deprecated {
background: var(--deprecated-bg);
border-color: var(--deprecated-border);
}
.admonition .admonition-title {
font-weight: 700;
margin-bottom: 0.25rem;
display: block;
}
/* ── Function/Member Lists ────────────────────────────────────────── */
.symbol-list {
list-style: none;
padding: 0;
}
.symbol-item {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin: 0.75rem 0;
transition: box-shadow var(--transition-speed);
}
.symbol-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.symbol-item .signature {
font-family: var(--font-mono);
font-size: 0.9375rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.symbol-item .brief {
color: var(--text-secondary);
font-size: 0.875rem;
}
.symbol-item .params {
margin-top: 0.5rem;
}
.symbol-item .param-row {
display: flex;
gap: 0.5rem;
font-size: 0.8125rem;
margin: 0.25rem 0;
}
.symbol-item .param-name {
font-family: var(--font-mono);
color: var(--link-color);
min-width: 100px;
}
.symbol-item .param-desc {
color: var(--text-tertiary);
}
.symbol-item .meta {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-tertiary);
}
.deprecated-badge {
background: var(--deprecated-bg);
color: #dc3545;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
}
.since-badge {
background: var(--note-bg);
color: var(--link-color);
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.6875rem;
margin-left: 0.5rem;
}
/* ── Inheritance Diagram ──────────────────────────────────────────── */
.inheritance-tree {
font-family: var(--font-mono);
font-size: 0.875rem;
padding: 1rem;
background: var(--bg-secondary);
border-radius: 6px;
overflow-x: auto;
}
.inheritance-tree .base-class {
color: var(--text-tertiary);
}
.inheritance-tree .current-class {
color: var(--link-color);
font-weight: 700;
}
.inheritance-tree .arrow {
color: var(--text-tertiary);
margin: 0 0.5rem;
}
/* ── Breadcrumb ───────────────────────────────────────────────────── */
.breadcrumb {
font-size: 0.8125rem;
color: var(--text-tertiary);
margin-bottom: 1rem;
}
.breadcrumb a { color: var(--text-secondary); }
.breadcrumb .separator { margin: 0 0.5rem; }
/* ── Index Grid ───────────────────────────────────────────────────── */
.index-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.index-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
transition: transform var(--transition-speed), box-shadow var(--transition-speed);
}
.index-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.index-card h4 { margin: 0 0 0.25rem; }
.index-card .kind { font-size: 0.75rem; color: var(--text-tertiary); }
.index-card .brief { font-size: 0.8125rem; color: var(--text-secondary); }
/* ── Print Styles ─────────────────────────────────────────────────── */
@media print {
.sidebar { display: none; }
.main-content { max-width: 100%; padding: 0; }
}
/* ── Responsive ───────────────────────────────────────────────────── */
@media (max-width: 768px) {
body { flex-direction: column; }
.sidebar {
width: 100%;
min-width: 100%;
height: auto;
position: static;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.main-content { padding: 1rem; }
}
"#;
pub const X86_DOC_DEFAULT_JS: &str = r#"
/* ── X86 Documentation Search ──────────────────────────────────────── */
(function() {
'use strict';
const searchIndex = [];
let searchTimeout = null;
function buildSearchIndex() {
document.querySelectorAll('.symbol-item').forEach(function(item) {
const name = item.getAttribute('data-name') || '';
const kind = item.getAttribute('data-kind') || '';
const brief = (item.querySelector('.brief') || {}).textContent || '';
const id = item.getAttribute('id') || '';
searchIndex.push({
name: name,
kind: kind,
brief: brief,
id: id,
element: item
});
});
}
function performSearch(query) {
const resultsContainer = document.getElementById('search-results');
if (!resultsContainer) return;
if (!query || query.trim().length < 2) {
resultsContainer.innerHTML = '';
document.querySelectorAll('.symbol-item').forEach(function(item) {
item.style.display = '';
});
return;
}
const q = query.toLowerCase();
const results = searchIndex.filter(function(entry) {
return entry.name.toLowerCase().includes(q) ||
entry.brief.toLowerCase().includes(q) ||
entry.kind.toLowerCase().includes(q);
});
document.querySelectorAll('.symbol-item').forEach(function(item) {
item.style.display = 'none';
});
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="search-no-results">No symbols found matching "' +
escapeHtml(query) + '"</div>';
} else {
let html = '';
results.forEach(function(r) {
html += '<div class="search-result-item" onclick="document.getElementById(\'' +
r.id + '\').scrollIntoView({behavior:\'smooth\'});" data-id="' + r.id + '">' +
'<strong>' + escapeHtml(r.name) + '</strong>' +
'<span class="kind">' + escapeHtml(r.kind) + '</span>' +
'<div style="font-size:0.75rem;color:var(--text-tertiary)">' +
escapeHtml(r.brief.substring(0, 80)) + (r.brief.length > 80 ? '...' : '') +
'</div></div>';
});
resultsContainer.innerHTML = html;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
function initSearch() {
const searchInput = document.getElementById('search-input');
if (!searchInput) return;
buildSearchIndex();
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
performSearch(searchInput.value);
}, 150);
});
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
searchInput.value = '';
performSearch('');
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSearch);
} else {
initSearch();
}
})();
"#;
pub const X86_DOC_MAX_BRIEF_LEN: usize = 256;
pub const X86_DOC_TARGET: &str = "x86_64-unknown-linux-gnu";
pub const X86_DOC_PAGE_ALIGNMENT: usize = 4096;
pub const X86_DOC_MAX_RECURSION: usize = 32;
#[derive(Debug, Clone, PartialEq)]
pub struct X86DocComment {
pub raw_text: String,
pub brief: Option<String>,
pub details: Option<String>,
pub params: Vec<(String, String)>,
pub tparams: Vec<(String, String)>,
pub return_desc: Option<String>,
pub retvals: Vec<(String, String)>,
pub notes: Vec<String>,
pub warnings: Vec<String>,
pub see_also: Vec<String>,
pub deprecated: bool,
pub deprecation_msg: Option<String>,
pub since: Option<String>,
pub throws: Vec<(String, String)>,
pub code_blocks: Vec<String>,
pub verbatim_blocks: Vec<String>,
pub markdown_body: String,
pub is_module_level: bool,
pub kind: X86CommentKind,
pub custom_tags: Vec<(String, String)>,
pub links: Vec<(String, String)>,
pub is_empty: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86CommentKind {
LineDoc,
BlockDoc,
ModuleDoc,
ModuleBlockDoc,
PostDoc,
}
impl fmt::Display for X86CommentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LineDoc => write!(f, "line-doc"),
Self::BlockDoc => write!(f, "block-doc"),
Self::ModuleDoc => write!(f, "module-doc"),
Self::ModuleBlockDoc => write!(f, "module-block-doc"),
Self::PostDoc => write!(f, "post-doc"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum DocCommandCategory {
Brief,
Details,
Param,
TParam,
Return,
RetVal,
Note,
Warning,
See,
Deprecated,
Since,
Throws,
Code,
EndCode,
Verbatim,
EndVerbatim,
Custom(String),
}
impl X86DocComment {
pub fn new() -> Self {
Self {
raw_text: String::new(),
brief: None,
details: None,
params: Vec::new(),
tparams: Vec::new(),
return_desc: None,
retvals: Vec::new(),
notes: Vec::new(),
warnings: Vec::new(),
see_also: Vec::new(),
deprecated: false,
deprecation_msg: None,
since: None,
throws: Vec::new(),
code_blocks: Vec::new(),
verbatim_blocks: Vec::new(),
markdown_body: String::new(),
is_module_level: false,
kind: X86CommentKind::LineDoc,
custom_tags: Vec::new(),
links: Vec::new(),
is_empty: true,
}
}
pub fn parse(raw: &str, kind: X86CommentKind) -> Self {
let mut comment = Self::new();
comment.raw_text = raw.to_string();
comment.kind = kind;
comment.is_module_level = matches!(
kind,
X86CommentKind::ModuleDoc | X86CommentKind::ModuleBlockDoc
);
if raw.trim().is_empty() {
comment.is_empty = true;
return comment;
}
let stripped = comment.strip_comment_markers(raw, kind);
if stripped.trim().is_empty() {
comment.is_empty = true;
return comment;
}
comment.is_empty = false;
let parsed = comment.parse_commands_and_markdown(&stripped);
comment.apply_parsed(parsed);
comment.extract_links();
comment
}
pub fn from_line_doc(raw: &str) -> Self {
Self::parse(raw, X86CommentKind::LineDoc)
}
pub fn from_block_doc(raw: &str) -> Self {
Self::parse(raw, X86CommentKind::BlockDoc)
}
pub fn from_module_doc(raw: &str) -> Self {
Self::parse(raw, X86CommentKind::ModuleDoc)
}
pub fn from_module_block_doc(raw: &str) -> Self {
Self::parse(raw, X86CommentKind::ModuleBlockDoc)
}
pub fn from_post_doc(raw: &str) -> Self {
Self::parse(raw, X86CommentKind::PostDoc)
}
pub fn merge_lines(lines: &[String]) -> Self {
let joined = lines.join("\n");
Self::from_line_doc(&joined)
}
fn strip_comment_markers(&self, raw: &str, kind: X86CommentKind) -> String {
match kind {
X86CommentKind::LineDoc => {
let mut result = String::new();
for line in raw.lines() {
let trimmed = line.trim();
if let Some(content) = trimmed.strip_prefix("///") {
let content = content.strip_prefix(' ').unwrap_or(content);
if !result.is_empty() {
result.push('\n');
}
result.push_str(content);
} else if let Some(content) = trimmed.strip_prefix("//!") {
let content = content.strip_prefix(' ').unwrap_or(content);
if !result.is_empty() {
result.push('\n');
}
result.push_str(content);
}
}
result
}
X86CommentKind::ModuleDoc => {
let mut result = String::new();
for line in raw.lines() {
let trimmed = line.trim();
if let Some(content) = trimmed.strip_prefix("//!") {
let content = content.strip_prefix(' ').unwrap_or(content);
if !result.is_empty() {
result.push('\n');
}
result.push_str(content);
}
}
result
}
X86CommentKind::BlockDoc | X86CommentKind::PostDoc => {
let inner = raw
.trim()
.strip_prefix("/**")
.and_then(|s| s.strip_suffix("*/"))
.or_else(|| {
raw.trim()
.strip_prefix("/*!")
.and_then(|s| s.strip_suffix("*/"))
})
.unwrap_or(raw);
Self::strip_block_leading_stars(inner)
}
X86CommentKind::ModuleBlockDoc => {
let inner = raw
.trim()
.strip_prefix("/*!")
.and_then(|s| s.strip_suffix("*/"))
.or_else(|| {
raw.trim()
.strip_prefix("/**")
.and_then(|s| s.strip_suffix("*/"))
})
.unwrap_or(raw);
Self::strip_block_leading_stars(inner)
}
}
}
fn strip_block_leading_stars(text: &str) -> String {
let mut result = String::new();
for line in text.lines() {
let trimmed = line.trim();
let content = if let Some(c) = trimmed.strip_prefix('*') {
c.strip_prefix(' ').unwrap_or(c)
} else {
trimmed
};
if !result.is_empty() {
result.push('\n');
}
result.push_str(content);
}
result
}
fn parse_commands_and_markdown(&self, text: &str) -> ParsedDoc {
let mut parsed = ParsedDoc::default();
let mut lines = text.lines().peekable();
let mut body_lines: Vec<String> = Vec::new();
let mut in_code_block = false;
let mut in_verbatim_block = false;
let mut code_buffer = String::new();
let mut verbatim_buffer = String::new();
let mut recursion_depth: usize = 0;
while let Some(line) = lines.next() {
if recursion_depth > X86_DOC_MAX_RECURSION {
body_lines.push(line.to_string());
continue;
}
recursion_depth += 1;
let trimmed = line.trim();
if in_code_block {
if trimmed == "\\endcode" || trimmed == "@endcode" {
in_code_block = false;
parsed.code_blocks.push(code_buffer.clone());
code_buffer.clear();
body_lines.push("```".to_string());
continue;
}
code_buffer.push_str(line);
code_buffer.push('\n');
body_lines.push(line.to_string());
continue;
}
if in_verbatim_block {
if trimmed == "\\endverbatim" || trimmed == "@endverbatim" {
in_verbatim_block = false;
parsed.verbatim_blocks.push(verbatim_buffer.clone());
verbatim_buffer.clear();
body_lines.push("```".to_string());
continue;
}
verbatim_buffer.push_str(line);
verbatim_buffer.push('\n');
body_lines.push(line.to_string());
continue;
}
if let Some(cmd_match) = Self::match_command(trimmed) {
let rest = &trimmed[cmd_match.len()..].trim().to_string();
match parse_command_category(&cmd_match) {
DocCommandCategory::Brief => {
if parsed.brief.is_none() {
parsed.brief = Some(rest.clone());
}
body_lines.push(rest.clone());
}
DocCommandCategory::Details => {
parsed.details.push_str(&rest);
parsed.details.push('\n');
body_lines.push(rest.clone());
}
DocCommandCategory::Param => {
if let Some((param_name, desc)) = rest.split_once(' ') {
parsed
.params
.push((param_name.trim().to_string(), desc.trim().to_string()));
} else if !rest.is_empty() {
parsed.params.push((rest.clone(), String::new()));
}
}
DocCommandCategory::TParam => {
if let Some((tparam_name, desc)) = rest.split_once(' ') {
parsed
.tparams
.push((tparam_name.trim().to_string(), desc.trim().to_string()));
} else if !rest.is_empty() {
parsed.tparams.push((rest.clone(), String::new()));
}
}
DocCommandCategory::Return => {
parsed.return_desc = Some(rest.clone());
}
DocCommandCategory::RetVal => {
if let Some((code, desc)) = rest.split_once(' ') {
parsed
.retvals
.push((code.trim().to_string(), desc.trim().to_string()));
} else if !rest.is_empty() {
parsed.retvals.push((rest.clone(), String::new()));
}
}
DocCommandCategory::Note => {
parsed.notes.push(rest.clone());
}
DocCommandCategory::Warning => {
parsed.warnings.push(rest.clone());
}
DocCommandCategory::See => {
parsed.see_also.push(rest.clone());
}
DocCommandCategory::Deprecated => {
parsed.deprecated = true;
if !rest.is_empty() {
parsed.deprecation_msg = Some(rest.clone());
}
}
DocCommandCategory::Since => {
if !rest.is_empty() {
parsed.since = Some(rest.clone());
}
}
DocCommandCategory::Throws => {
if let Some((exc_type, desc)) = rest.split_once(' ') {
parsed
.throws
.push((exc_type.trim().to_string(), desc.trim().to_string()));
} else if !rest.is_empty() {
parsed.throws.push((rest.clone(), String::new()));
}
}
DocCommandCategory::Code => {
in_code_block = true;
code_buffer.clear();
body_lines.push("```".to_string());
}
DocCommandCategory::Verbatim => {
in_verbatim_block = true;
verbatim_buffer.clear();
body_lines.push("```".to_string());
}
DocCommandCategory::Custom(tag_name) => {
parsed.custom_tags.push((tag_name, rest.clone()));
}
_ => {
body_lines.push(line.to_string());
}
}
} else {
body_lines.push(line.to_string());
}
}
if in_code_block && !code_buffer.is_empty() {
parsed.code_blocks.push(code_buffer);
}
if in_verbatim_block && !verbatim_buffer.is_empty() {
parsed.verbatim_blocks.push(verbatim_buffer);
}
parsed.markdown_body = body_lines.join("\n");
if parsed.brief.is_none() {
parsed.brief = Self::extract_first_paragraph(&parsed.markdown_body);
}
parsed
}
fn match_command(line: &str) -> Option<String> {
let line = line.trim();
if let Some(cmd) = line.strip_prefix('\\') {
let end = cmd.find(|c: char| c.is_whitespace()).unwrap_or(cmd.len());
let cmd_name = &cmd[..end];
if is_known_command(cmd_name) {
return Some(format!("\\{}", cmd_name));
}
return Some(format!("\\{}", cmd_name));
}
if let Some(cmd) = line.strip_prefix('@') {
let end = cmd.find(|c: char| c.is_whitespace()).unwrap_or(cmd.len());
let cmd_name = &cmd[..end];
if is_known_command(cmd_name) {
return Some(format!("@{}", cmd_name));
}
return Some(format!("@{}", cmd_name));
}
None
}
fn extract_first_paragraph(text: &str) -> Option<String> {
let mut first = String::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() && !first.is_empty() {
break;
}
if !trimmed.is_empty() && !trimmed.starts_with('\\') && !trimmed.starts_with('@') {
if !first.is_empty() {
first.push(' ');
}
first.push_str(trimmed);
}
}
if first.is_empty() {
None
} else if first.len() > X86_DOC_MAX_BRIEF_LEN {
Some(format!(
"{}...",
&first[..X86_DOC_MAX_BRIEF_LEN.min(first.len())]
))
} else {
Some(first)
}
}
fn apply_parsed(&mut self, parsed: ParsedDoc) {
self.brief = parsed.brief;
self.details = if parsed.details.trim().is_empty() {
None
} else {
Some(parsed.details.trim().to_string())
};
self.params = parsed.params;
self.tparams = parsed.tparams;
self.return_desc = parsed.return_desc;
self.retvals = parsed.retvals;
self.notes = parsed.notes;
self.warnings = parsed.warnings;
self.see_also = parsed.see_also;
self.deprecated = parsed.deprecated;
self.deprecation_msg = parsed.deprecation_msg;
self.since = parsed.since;
self.throws = parsed.throws;
self.code_blocks = parsed.code_blocks;
self.verbatim_blocks = parsed.verbatim_blocks;
self.markdown_body = parsed.markdown_body;
self.custom_tags = parsed.custom_tags;
}
fn extract_links(&mut self) {
let mut links = Vec::new();
let chars: Vec<char> = self.markdown_body.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' {
let mut j = i + 1;
let mut link_text = String::new();
while j < chars.len() && chars[j] != ']' {
link_text.push(chars[j]);
j += 1;
}
if j < chars.len() && chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == '('
{
let mut k = j + 2;
let mut url = String::new();
while k < chars.len() && chars[k] != ')' {
url.push(chars[k]);
k += 1;
}
if k < chars.len() {
links.push((link_text, url));
i = k;
}
}
}
i += 1;
}
self.links = links;
}
pub fn to_html(&self) -> String {
self.render_markdown_to_html(&self.markdown_body)
}
fn render_markdown_to_html(&self, text: &str) -> String {
let mut html = String::new();
let lines: Vec<&str> = text.lines().collect();
let mut i = 0;
let mut in_code_fence = false;
let mut fence_lang = String::new();
let mut in_list = false;
while i < lines.len() {
let line = lines[i];
if line.trim_start().starts_with("```") {
if !in_code_fence {
in_code_fence = true;
fence_lang = line
.trim_start()
.trim_start_matches("```")
.trim()
.to_string();
html.push_str("<pre><code");
if !fence_lang.is_empty() {
html.push_str(&format!(" class=\"language-{}\"", fence_lang));
}
html.push('>');
if i + 1 < lines.len() {
html.push('\n');
}
} else {
in_code_fence = false;
fence_lang.clear();
html.push_str("</code></pre>\n");
}
i += 1;
continue;
}
if in_code_fence {
html.push_str(&Self::html_escape(line));
html.push('\n');
i += 1;
continue;
}
if let Some(heading) = self.parse_markdown_heading(line) {
if in_list {
html.push_str("</ul>\n");
in_list = false;
}
html.push_str(&heading);
html.push('\n');
i += 1;
continue;
}
let trimmed = line.trim();
if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
if !in_list {
html.push_str("<ul>\n");
in_list = true;
}
let content = &trimmed[2..];
html.push_str("<li>");
html.push_str(&Self::render_inline_markdown(content));
html.push_str("</li>\n");
i += 1;
continue;
}
if let Some(ordered_content) = trimmed.strip_prefix(|c: char| c.is_ascii_digit()) {
if let Some(content) = ordered_content.strip_prefix(". ") {
if !in_list {
html.push_str("<ol>\n");
in_list = true;
}
html.push_str("<li>");
html.push_str(&Self::render_inline_markdown(content));
html.push_str("</li>\n");
i += 1;
continue;
}
}
if in_list && trimmed.is_empty() {
i += 1;
continue;
}
if in_list
&& !trimmed.is_empty()
&& !trimmed.starts_with("- ")
&& !trimmed.starts_with("* ")
{
html.push_str(if html.contains("<ol>") {
"</ol>\n"
} else {
"</ul>\n"
});
in_list = false;
}
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
html.push_str("<hr>\n");
i += 1;
continue;
}
if trimmed.starts_with("> ") {
html.push_str("<blockquote>");
html.push_str(&Self::render_inline_markdown(&trimmed[2..]));
html.push_str("</blockquote>\n");
i += 1;
continue;
}
if trimmed.starts_with('|') && trimmed.ends_with('|') {
html.push_str(&self.render_markdown_table(&lines, &mut i));
continue;
}
if trimmed.is_empty() {
html.push_str("<br>\n");
i += 1;
continue;
}
html.push_str("<p>");
html.push_str(&Self::render_inline_markdown(trimmed));
html.push_str("</p>\n");
i += 1;
}
if in_list {
html.push_str(if html.contains("<ol>") {
"</ol>\n"
} else {
"</ul>\n"
});
}
if in_code_fence {
html.push_str("</code></pre>\n");
}
html
}
fn parse_markdown_heading(&self, line: &str) -> Option<String> {
let trimmed = line.trim();
if let Some(content) = trimmed.strip_prefix("###### ") {
Some(format!(
"<h6>{}</h6>",
Self::render_inline_markdown(content)
))
} else if let Some(content) = trimmed.strip_prefix("##### ") {
Some(format!(
"<h5>{}</h5>",
Self::render_inline_markdown(content)
))
} else if let Some(content) = trimmed.strip_prefix("#### ") {
Some(format!(
"<h4>{}</h4>",
Self::render_inline_markdown(content)
))
} else if let Some(content) = trimmed.strip_prefix("### ") {
Some(format!(
"<h3>{}</h3>",
Self::render_inline_markdown(content)
))
} else if let Some(content) = trimmed.strip_prefix("## ") {
Some(format!(
"<h2>{}</h2>",
Self::render_inline_markdown(content)
))
} else if let Some(content) = trimmed.strip_prefix("# ") {
Some(format!(
"<h1>{}</h1>",
Self::render_inline_markdown(content)
))
} else if trimmed.starts_with("===") {
Some("<h1></h1>".to_string())
} else if trimmed.starts_with("---") {
None
} else {
None
}
}
fn render_markdown_table(&self, lines: &[&str], i: &mut usize) -> String {
let mut table_rows: Vec<&str> = Vec::new();
while *i < lines.len() {
let line = lines[*i].trim();
if !line.starts_with('|') || !line.ends_with('|') {
break;
}
table_rows.push(line);
*i += 1;
}
if table_rows.is_empty() {
return String::new();
}
let mut html = String::from("<table>\n");
if !table_rows.is_empty() {
html.push_str("<thead><tr>");
for cell in table_rows[0].split('|').filter(|c| !c.trim().is_empty()) {
html.push_str("<th>");
html.push_str(&Self::render_inline_markdown(cell.trim()));
html.push_str("</th>");
}
html.push_str("</tr></thead>\n");
}
let start = if table_rows.len() > 1 && table_rows[1].contains("---") {
2
} else {
1
};
if start < table_rows.len() {
html.push_str("<tbody>\n");
for row in &table_rows[start..] {
html.push_str("<tr>");
for cell in row.split('|').filter(|c| !c.trim().is_empty()) {
html.push_str("<td>");
html.push_str(&Self::render_inline_markdown(cell.trim()));
html.push_str("</td>");
}
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n");
}
html.push_str("</table>\n");
html
}
fn render_inline_markdown(text: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '`' {
let mut j = i + 1;
let mut code = String::new();
while j < chars.len() && chars[j] != '`' {
code.push(chars[j]);
j += 1;
}
if j < chars.len() {
result.push_str("<code>");
result.push_str(&Self::html_escape(&code));
result.push_str("</code>");
i = j + 1;
continue;
}
}
if (i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*')
|| (i + 1 < chars.len() && chars[i] == '_' && chars[i + 1] == '_')
{
let marker = if chars[i] == '*' { "**" } else { "__" };
let mut j = i + 2;
while j + 1 < chars.len() {
if chars[j] == marker.chars().next().unwrap()
&& chars[j + 1] == marker.chars().next().unwrap()
{
let bold_text = &text[i + 2..j];
result.push_str("<strong>");
result.push_str(&Self::render_inline_markdown(bold_text));
result.push_str("</strong>");
i = j + 2;
break;
}
j += 1;
}
if j >= chars.len() - 1 {
result.push(chars[i]);
i += 1;
}
continue;
}
if chars[i] == '*' || chars[i] == '_' {
let marker = chars[i];
let mut j = i + 1;
while j < chars.len() && chars[j] != marker {
j += 1;
}
if j < chars.len() && j > i + 1 {
let italic_text = &text[i + 1..j];
result.push_str("<em>");
result.push_str(&Self::render_inline_markdown(italic_text));
result.push_str("</em>");
i = j + 1;
continue;
}
}
if chars[i] == '[' {
let mut j = i + 1;
let mut link_text = String::new();
while j < chars.len() && chars[j] != ']' {
link_text.push(chars[j]);
j += 1;
}
if j < chars.len() && chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == '('
{
let mut k = j + 2;
let mut url = String::new();
while k < chars.len() && chars[k] != ')' {
url.push(chars[k]);
k += 1;
}
if k < chars.len() {
result.push_str(&format!(
"<a href=\"{}\">{}</a>",
Self::html_escape(&url),
Self::render_inline_markdown(&link_text)
));
i = k + 1;
continue;
}
}
}
if chars[i] == '<' {
result.push_str("<");
} else if chars[i] == '>' {
result.push_str(">");
} else if chars[i] == '&' {
result.push_str("&");
} else {
result.push(chars[i]);
}
i += 1;
}
result
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
pub fn has_content(&self) -> bool {
!self.is_empty
}
pub fn validate(&self) -> Vec<X86DocWarning> {
let mut warnings = Vec::new();
if self.is_empty {
warnings.push(X86DocWarning::Undocumented);
return warnings;
}
if self.brief.is_none() {
warnings.push(X86DocWarning::MissingBrief);
}
if self.return_desc.is_none() && self.markdown_body.to_lowercase().contains("return") {
warnings.push(X86DocWarning::MissingReturnDoc);
}
if self.deprecated {
warnings.push(X86DocWarning::DeprecatedSymbol);
}
warnings
}
}
impl Default for X86DocComment {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Clone)]
struct ParsedDoc {
brief: Option<String>,
details: String,
params: Vec<(String, String)>,
tparams: Vec<(String, String)>,
return_desc: Option<String>,
retvals: Vec<(String, String)>,
notes: Vec<String>,
warnings: Vec<String>,
see_also: Vec<String>,
deprecated: bool,
deprecation_msg: Option<String>,
since: Option<String>,
throws: Vec<(String, String)>,
code_blocks: Vec<String>,
verbatim_blocks: Vec<String>,
markdown_body: String,
custom_tags: Vec<(String, String)>,
}
fn parse_command_category(cmd: &str) -> DocCommandCategory {
let name = cmd
.strip_prefix('\\')
.or_else(|| cmd.strip_prefix('@'))
.unwrap_or(cmd)
.to_lowercase();
match name.as_str() {
"brief" => DocCommandCategory::Brief,
"details" | "detail" => DocCommandCategory::Details,
"param" | "parameter" | "arg" | "argument" => DocCommandCategory::Param,
"tparam" => DocCommandCategory::TParam,
"return" | "returns" | "result" => DocCommandCategory::Return,
"retval" => DocCommandCategory::RetVal,
"note" | "remark" => DocCommandCategory::Note,
"warning" | "warn" => DocCommandCategory::Warning,
"see" | "sa" | "seealso" => DocCommandCategory::See,
"deprecated" | "deprec" => DocCommandCategory::Deprecated,
"since" => DocCommandCategory::Since,
"throws" | "throw" | "exception" => DocCommandCategory::Throws,
"code" | "codeblock" => DocCommandCategory::Code,
"endcode" => DocCommandCategory::EndCode,
"verbatim" => DocCommandCategory::Verbatim,
"endverbatim" => DocCommandCategory::EndVerbatim,
other => DocCommandCategory::Custom(other.to_string()),
}
}
fn is_known_command(name: &str) -> bool {
let lower = name.to_lowercase();
matches!(
lower.as_str(),
"brief"
| "details"
| "detail"
| "param"
| "parameter"
| "arg"
| "argument"
| "tparam"
| "return"
| "returns"
| "result"
| "retval"
| "note"
| "remark"
| "warning"
| "warn"
| "see"
| "sa"
| "seealso"
| "deprecated"
| "deprec"
| "since"
| "throws"
| "throw"
| "exception"
| "code"
| "codeblock"
| "endcode"
| "verbatim"
| "endverbatim"
| "file"
| "author"
| "version"
| "date"
| "copyright"
| "todo"
| "test"
| "bug"
)
}
#[derive(Debug, Clone)]
pub struct X86DocHTML {
pub project_name: String,
pub project_version: String,
pub project_brief: Option<String>,
pub output_dir: PathBuf,
pub custom_css: Option<String>,
pub custom_js: Option<String>,
pub generate_search: bool,
pub responsive: bool,
pub dark_mode: bool,
pages: Vec<X86DocPage>,
symbol_index: Vec<X86DocSymbolEntry>,
nav_tree: X86DocNavTree,
}
#[derive(Debug, Clone)]
pub struct X86DocPage {
pub title: String,
pub kind: X86DocPageKind,
pub output_file: PathBuf,
pub breadcrumb: Vec<(String, String)>,
pub body_html: String,
pub symbols: Vec<X86DocSymbolEntry>,
pub sub_pages: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86DocPageKind {
Index,
Namespace,
Class,
Function,
Enum,
TypeDef,
File,
Module,
HierarchicalIndex,
Search,
Topic,
}
impl fmt::Display for X86DocPageKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Index => write!(f, "index"),
Self::Namespace => write!(f, "namespace"),
Self::Class => write!(f, "class"),
Self::Function => write!(f, "function"),
Self::Enum => write!(f, "enum"),
Self::TypeDef => write!(f, "typedef"),
Self::File => write!(f, "file"),
Self::Module => write!(f, "module"),
Self::HierarchicalIndex => write!(f, "hierarchy"),
Self::Search => write!(f, "search"),
Self::Topic => write!(f, "topic"),
}
}
}
#[derive(Debug, Clone)]
pub struct X86DocSymbolEntry {
pub qualified_name: String,
pub name: String,
pub kind: X86DocSymbolKind,
pub brief: Option<String>,
pub file: Option<String>,
pub line: Option<usize>,
pub parent: Option<String>,
pub link: String,
pub access: Option<X86DocAccess>,
pub template_params: Vec<String>,
pub signature: Option<String>,
pub return_type: Option<String>,
pub parameters: Vec<(String, String)>,
pub deprecated: bool,
pub since: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86DocSymbolKind {
Namespace,
Class,
Struct,
Union,
Enum,
EnumValue,
Function,
Method,
Constructor,
Destructor,
Operator,
Variable,
Field,
TypeDef,
Using,
Macro,
Module,
Concept,
Template,
Alias,
}
impl fmt::Display for X86DocSymbolKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Namespace => write!(f, "namespace"),
Self::Class => write!(f, "class"),
Self::Struct => write!(f, "struct"),
Self::Union => write!(f, "union"),
Self::Enum => write!(f, "enum"),
Self::EnumValue => write!(f, "enumerator"),
Self::Function => write!(f, "function"),
Self::Method => write!(f, "method"),
Self::Constructor => write!(f, "constructor"),
Self::Destructor => write!(f, "destructor"),
Self::Operator => write!(f, "operator"),
Self::Variable => write!(f, "variable"),
Self::Field => write!(f, "field"),
Self::TypeDef => write!(f, "typedef"),
Self::Using => write!(f, "using"),
Self::Macro => write!(f, "macro"),
Self::Module => write!(f, "module"),
Self::Concept => write!(f, "concept"),
Self::Template => write!(f, "template"),
Self::Alias => write!(f, "alias"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86DocAccess {
Public,
Protected,
Private,
}
impl fmt::Display for X86DocAccess {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Public => write!(f, "public"),
Self::Protected => write!(f, "protected"),
Self::Private => write!(f, "private"),
}
}
}
#[derive(Debug, Clone)]
struct X86DocNavNode {
name: String,
link: Option<String>,
kind: X86DocSymbolKind,
children: Vec<X86DocNavNode>,
collapsed: bool,
}
#[derive(Debug, Clone, Default)]
struct X86DocNavTree {
roots: Vec<X86DocNavNode>,
}
impl X86DocHTML {
pub fn new(project_name: &str, output_dir: PathBuf) -> Self {
Self {
project_name: project_name.to_string(),
project_version: "0.1.0".to_string(),
project_brief: None,
output_dir,
custom_css: None,
custom_js: None,
generate_search: true,
responsive: true,
dark_mode: true,
pages: Vec::new(),
symbol_index: Vec::new(),
nav_tree: X86DocNavTree::default(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.project_name.trim().is_empty() {
return Err("Project name cannot be empty".to_string());
}
Ok(())
}
pub fn add_page(&mut self, page: X86DocPage) {
for sym in &page.symbols {
self.symbol_index.push(sym.clone());
}
self.pages.push(page);
}
pub fn build_nav_tree(&mut self) {
let mut roots: Vec<X86DocNavNode> = Vec::new();
let mut node_map: HashMap<String, usize> = HashMap::new();
for entry in &self.symbol_index {
let parts: Vec<&str> = entry.qualified_name.split("::").collect();
for (depth, part) in parts.iter().enumerate() {
let full_path = parts[..=depth].join("::");
if !node_map.contains_key(&full_path) {
let node = X86DocNavNode {
name: part.to_string(),
link: if depth == parts.len() - 1 {
Some(entry.link.clone())
} else {
None
},
kind: if depth == parts.len() - 1 {
entry.kind
} else {
X86DocSymbolKind::Namespace
},
children: Vec::new(),
collapsed: depth > 1,
};
if depth == 0 {
roots.push(node);
node_map.insert(full_path.clone(), roots.len() - 1);
} else {
let parent_path = parts[..depth].join("::");
if let Some(&parent_idx) = node_map.get(&parent_path) {
if parent_idx < roots.len() {
fn push_at_depth(
node: &mut X86DocNavNode,
depth: usize,
new_child: X86DocNavNode,
) {
if depth == 0 {
node.children.push(new_child);
} else if let Some(last) = node.children.last_mut() {
push_at_depth(last, depth - 1, new_child);
}
}
let new_child = node;
push_at_depth(&mut roots[parent_idx], depth - 1, new_child);
}
}
}
}
}
}
self.nav_tree = X86DocNavTree { roots };
}
pub fn generate(&self) -> Result<Vec<(PathBuf, String)>, String> {
let mut output = Vec::new();
let css = self.build_css();
output.push((PathBuf::from("x86-doc-style.css"), css));
if self.generate_search {
let js = self.build_js();
output.push((PathBuf::from("x86-doc-search.js"), js));
}
for page in &self.pages {
let html = self.render_page(page);
output.push((page.output_file.clone(), html));
}
if !self.pages.iter().any(|p| p.kind == X86DocPageKind::Index) {
let index_html = self.render_index_page();
output.push((PathBuf::from("index.html"), index_html));
}
if self.generate_search && !self.pages.iter().any(|p| p.kind == X86DocPageKind::Search) {
let search_html = self.render_search_page();
output.push((PathBuf::from("search.html"), search_html));
}
Ok(output)
}
fn render_page(&self, page: &X86DocPage) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n");
html.push_str("<html lang=\"en\">\n");
html.push_str("<head>\n");
html.push_str("<meta charset=\"UTF-8\">\n");
html.push_str(
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n",
);
html.push_str(&format!(
"<title>{} — {}</title>\n",
page.title, self.project_name
));
html.push_str("<link rel=\"stylesheet\" href=\"x86-doc-style.css\">\n");
if self.generate_search {
html.push_str("<script src=\"x86-doc-search.js\" defer></script>\n");
}
html.push_str("</head>\n<body>\n");
html.push_str(&self.render_sidebar(page));
html.push_str("<main class=\"main-content\">\n");
html.push_str(&self.render_breadcrumb(&page.breadcrumb));
html.push_str("<div class=\"doc-header\">\n");
html.push_str(&format!("<h1>{}</h1>\n", Self::html_escape(&page.title)));
if let Some(ref brief) = self.project_brief {
html.push_str(&format!(
"<p class=\"subtitle\">{}</p>\n",
Self::html_escape(brief)
));
}
html.push_str("</div>\n");
html.push_str(&page.body_html);
html.push_str("</main>\n</body>\n</html>");
html
}
fn render_sidebar(&self, _page: &X86DocPage) -> String {
let mut html = String::new();
html.push_str("<nav class=\"sidebar\">\n");
html.push_str(&format!(
"<h2 style=\"font-size:1.1rem;margin-bottom:0.5rem\">{}</h2>\n",
Self::html_escape(&self.project_name)
));
html.push_str(&format!(
"<div style=\"font-size:0.75rem;color:var(--text-tertiary);margin-bottom:1rem\">v{}</div>\n",
Self::html_escape(&self.project_version)
));
if self.generate_search {
html.push_str("<div class=\"search-container\">\n");
html.push_str("<input type=\"text\" id=\"search-input\" placeholder=\"Search symbols...\" autocomplete=\"off\">\n");
html.push_str("<div id=\"search-results\" class=\"search-results\"></div>\n");
html.push_str("</div>\n");
}
html.push_str("<h3>Contents</h3>\n");
html.push_str("<ul>\n");
for root in &self.nav_tree.roots {
html.push_str(&self.render_nav_node(root, 0));
}
html.push_str("</ul>\n");
html.push_str("<h3>Pages</h3>\n");
html.push_str("<ul>\n");
html.push_str("<li><a href=\"index.html\">Home</a></li>\n");
html.push_str("<li><a href=\"hierarchy.html\">Class Hierarchy</a></li>\n");
if self.generate_search {
html.push_str("<li><a href=\"search.html\">Search</a></li>\n");
}
html.push_str("</ul>\n");
html.push_str("</nav>\n");
html
}
fn render_nav_node(&self, node: &X86DocNavNode, depth: usize) -> String {
let mut html = String::new();
html.push_str("<li");
if depth > 0 {
html.push_str(" class=\"nested\"");
}
html.push('>');
if let Some(ref link) = node.link {
html.push_str(&format!(
"<a href=\"{}\">{}</a>",
Self::html_escape(link),
Self::html_escape(&node.name)
));
} else {
html.push_str(&format!(
"<span class=\"nav-folder\">{}</span>",
Self::html_escape(&node.name)
));
}
if !node.children.is_empty() {
html.push_str("<ul>\n");
for child in &node.children {
html.push_str(&self.render_nav_node(child, depth + 1));
}
html.push_str("</ul>\n");
}
html.push_str("</li>\n");
html
}
fn render_breadcrumb(&self, crumbs: &[(String, String)]) -> String {
if crumbs.is_empty() {
return String::new();
}
let mut html = String::from("<div class=\"breadcrumb\">\n");
for (i, (name, link)) in crumbs.iter().enumerate() {
if i > 0 {
html.push_str("<span class=\"separator\">/</span>");
}
if !link.is_empty() {
html.push_str(&format!(
"<a href=\"{}\">{}</a>",
Self::html_escape(link),
Self::html_escape(name)
));
} else {
html.push_str(&Self::html_escape(name));
}
}
html.push_str("\n</div>\n");
html
}
fn render_index_page(&self) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
html.push_str("<meta charset=\"UTF-8\">\n");
html.push_str(
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n",
);
html.push_str(&format!(
"<title>{} Documentation</title>\n",
self.project_name
));
html.push_str("<link rel=\"stylesheet\" href=\"x86-doc-style.css\">\n");
if self.generate_search {
html.push_str("<script src=\"x86-doc-search.js\" defer></script>\n");
}
html.push_str("</head>\n<body>\n");
html.push_str("<nav class=\"sidebar\">\n");
html.push_str(&format!(
"<h2>{}</h2>\n",
Self::html_escape(&self.project_name)
));
html.push_str(&format!(
"<div style=\"font-size:0.75rem;color:var(--text-tertiary);margin-bottom:1rem\">v{}</div>\n",
Self::html_escape(&self.project_version)
));
html.push_str("</nav>\n");
html.push_str("<main class=\"main-content\">\n");
html.push_str("<div class=\"doc-header\">\n");
html.push_str(&format!("<h1>{} Documentation</h1>\n", self.project_name));
if let Some(ref brief) = self.project_brief {
html.push_str(&format!(
"<p class=\"subtitle\">{}</p>\n",
Self::html_escape(brief)
));
}
html.push_str("</div>\n");
html.push_str("<div class=\"index-grid\">\n");
for page in &self.pages {
if page.kind == X86DocPageKind::Index {
continue;
}
html.push_str("<a href=\"");
html.push_str(&page.output_file.to_string_lossy());
html.push_str("\" class=\"index-card\">\n");
html.push_str(&format!("<h4>{}</h4>\n", Self::html_escape(&page.title)));
html.push_str(&format!("<span class=\"kind\">{}</span>\n", page.kind));
html.push_str("</a>\n");
}
html.push_str("</div>\n");
html.push_str("<h2>All Symbols</h2>\n");
html.push_str("<table>\n<thead><tr><th>Name</th><th>Kind</th><th>Description</th></tr></thead>\n<tbody>\n");
for sym in &self.symbol_index {
html.push_str("<tr>\n");
html.push_str(&format!(
"<td><a href=\"{}\">{}</a></td>\n",
Self::html_escape(&sym.link),
Self::html_escape(&sym.qualified_name)
));
html.push_str(&format!("<td>{}</td>\n", sym.kind));
html.push_str(&format!(
"<td>{}</td>\n",
Self::html_escape(sym.brief.as_deref().unwrap_or(""))
));
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n</table>\n");
html.push_str("</main>\n</body>\n</html>");
html
}
fn render_search_page(&self) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
html.push_str("<meta charset=\"UTF-8\">\n");
html.push_str("<title>Search — ");
html.push_str(&Self::html_escape(&self.project_name));
html.push_str("</title>\n");
html.push_str("<link rel=\"stylesheet\" href=\"x86-doc-style.css\">\n");
html.push_str("</head>\n<body>\n");
html.push_str("<nav class=\"sidebar\">\n");
html.push_str("<h2>Search</h2>\n");
html.push_str("<p><a href=\"index.html\">← Back to Index</a></p>\n");
html.push_str("</nav>\n");
html.push_str("<main class=\"main-content\">\n");
html.push_str("<div class=\"doc-header\"><h1>Search</h1></div>\n");
html.push_str("<div class=\"search-container\" style=\"max-width:600px;margin:2rem 0\">\n");
html.push_str("<input type=\"text\" id=\"search-input\" placeholder=\"Type to search...\" autocomplete=\"off\" style=\"width:100%;padding:0.75rem 1rem;font-size:1.1rem\">\n");
html.push_str("<div id=\"search-results\"></div>\n");
html.push_str("</div>\n");
html.push_str("<script src=\"x86-doc-search.js\"></script>\n");
html.push_str("</main>\n</body>\n</html>");
html
}
pub fn render_class_page(
&mut self,
name: &str,
qualified_name: &str,
kind: X86DocSymbolKind,
brief: Option<&str>,
members: &[X86DocSymbolEntry],
methods: &[X86DocSymbolEntry],
bases: &[String],
namespace: Option<&str>,
) -> String {
let mut html = String::new();
if let Some(b) = brief {
html.push_str(&format!(
"<p class=\"brief\">{}</p>\n",
Self::html_escape(b)
));
}
html.push_str(&format!(
"<p><span class=\"kind-badge\">{}</span></p>\n",
kind
));
if let Some(ns) = namespace {
html.push_str(&format!(
"<p>Namespace: <code>{}</code></p>\n",
Self::html_escape(ns)
));
}
if !bases.is_empty() {
html.push_str("<h2>Inheritance</h2>\n");
html.push_str("<div class=\"inheritance-tree\">\n");
for (i, base) in bases.iter().enumerate() {
if i > 0 {
html.push_str("<span class=\"arrow\">→</span>");
}
html.push_str(&format!(
"<span class=\"base-class\">{}</span>",
Self::html_escape(base)
));
}
html.push_str("<span class=\"arrow\">→</span>");
html.push_str(&format!(
"<span class=\"current-class\">{}</span>",
Self::html_escape(name)
));
html.push_str("\n</div>\n");
}
if !members.is_empty() {
html.push_str("<h2>Members</h2>\n");
html.push_str(&self.render_symbol_list(members));
}
if !methods.is_empty() {
html.push_str("<h2>Methods</h2>\n");
html.push_str(&self.render_symbol_list(methods));
}
let file_name = format!(
"class_{}.html",
qualified_name.replace("::", "_").to_lowercase()
);
let page = X86DocPage {
title: format!("{} {}", kind, name),
kind: X86DocPageKind::Class,
output_file: PathBuf::from(&file_name),
breadcrumb: vec![
("Home".to_string(), "index.html".to_string()),
(name.to_string(), String::new()),
],
body_html: html.clone(),
symbols: members.iter().chain(methods.iter()).cloned().collect(),
sub_pages: Vec::new(),
};
self.add_page(page);
html
}
pub fn render_function_page(
&mut self,
name: &str,
signature: &str,
return_type: &str,
params: &[(String, String)],
brief: Option<&str>,
comment: Option<&X86DocComment>,
) -> String {
let mut html = String::new();
html.push_str(
"<div class=\"symbol-item\" style=\"border:2px solid var(--border-color)\">\n",
);
html.push_str(&format!(
"<div class=\"signature\" style=\"font-size:1.1rem\">{}</div>\n",
Self::html_escape(signature)
));
html.push_str("</div>\n");
if let Some(b) = brief {
html.push_str(&format!(
"<p class=\"brief\">{}</p>\n",
Self::html_escape(b)
));
}
if let Some(c) = comment {
if !c.markdown_body.is_empty() {
html.push_str("<h2>Description</h2>\n");
html.push_str(&c.to_html());
}
html.push_str("<h2>Parameters</h2>\n");
if params.is_empty() && c.params.is_empty() {
html.push_str("<p><em>None</em></p>\n");
} else {
html.push_str("<table>\n<thead><tr><th>Parameter</th><th>Description</th></tr></thead>\n<tbody>\n");
for (pname, ptype) in params {
let desc = c
.params
.iter()
.find(|(n, _)| n == pname)
.map(|(_, d)| d.as_str())
.unwrap_or("");
html.push_str("<tr>");
html.push_str(&format!(
"<td><code>{}</code> <span style=\"color:var(--text-tertiary);font-size:0.8em\">{}</span></td>",
Self::html_escape(pname),
Self::html_escape(ptype)
));
html.push_str(&format!("<td>{}</td>", Self::html_escape(desc)));
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n</table>\n");
}
html.push_str("<h2>Returns</h2>\n");
html.push_str(&format!(
"<p><code>{}</code>",
Self::html_escape(return_type)
));
if let Some(ref ret) = c.return_desc {
html.push_str(&format!(" — {}", Self::html_escape(ret)));
}
html.push_str("</p>\n");
if !c.retvals.is_empty() {
html.push_str("<h3>Return Values</h3>\n");
html.push_str("<table>\n<tbody>\n");
for (code, desc) in &c.retvals {
html.push_str("<tr>");
html.push_str(&format!(
"<td><code>{}</code></td>",
Self::html_escape(code)
));
html.push_str(&format!("<td>{}</td>", Self::html_escape(desc)));
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n</table>\n");
}
if !c.notes.is_empty() {
html.push_str("<h2>Notes</h2>\n");
for note in &c.notes {
html.push_str(&format!(
"<div class=\"admonition admonition-note\"><span class=\"admonition-title\">Note</span>{}</div>\n",
Self::html_escape(note)
));
}
}
if !c.warnings.is_empty() {
html.push_str("<h2>Warnings</h2>\n");
for warn in &c.warnings {
html.push_str(&format!(
"<div class=\"admonition admonition-warning\"><span class=\"admonition-title\">Warning</span>{}</div>\n",
Self::html_escape(warn)
));
}
}
if !c.see_also.is_empty() {
html.push_str("<h2>See Also</h2>\n<ul>\n");
for see in &c.see_also {
html.push_str(&format!("<li>{}</li>\n", Self::html_escape(see)));
}
html.push_str("</ul>\n");
}
}
html
}
pub fn render_namespace_page(
&mut self,
name: &str,
contents: &[X86DocSymbolEntry],
nested_namespaces: &[String],
brief: Option<&str>,
) -> String {
let mut html = String::new();
if let Some(b) = brief {
html.push_str(&format!(
"<p class=\"brief\">{}</p>\n",
Self::html_escape(b)
));
}
if !nested_namespaces.is_empty() {
html.push_str("<h2>Nested Namespaces</h2>\n<ul>\n");
for ns in nested_namespaces {
let link = format!("namespace_{}.html", ns.replace("::", "_"));
html.push_str(&format!(
"<li><a href=\"{}\">{}</a></li>\n",
Self::html_escape(&link),
Self::html_escape(ns)
));
}
html.push_str("</ul>\n");
}
html.push_str("<h2>Contents</h2>\n");
html.push_str(&self.render_symbol_list(contents));
html
}
pub fn render_file_page(
&mut self,
filename: &str,
includes: &[String],
functions: &[X86DocSymbolEntry],
classes: &[X86DocSymbolEntry],
macros: &[X86DocSymbolEntry],
) -> String {
let mut html = String::new();
if !includes.is_empty() {
html.push_str("<h2>Includes</h2>\n<ul>\n");
for inc in includes {
html.push_str(&format!(
"<li><code>{}</code></li>\n",
Self::html_escape(inc)
));
}
html.push_str("</ul>\n");
}
if !classes.is_empty() {
html.push_str("<h2>Classes & Structs</h2>\n");
html.push_str(&self.render_symbol_list(classes));
}
if !functions.is_empty() {
html.push_str("<h2>Functions</h2>\n");
html.push_str(&self.render_symbol_list(functions));
}
if !macros.is_empty() {
html.push_str("<h2>Macros</h2>\n");
html.push_str(&self.render_symbol_list(macros));
}
html
}
pub fn render_module_page(
&mut self,
module_name: &str,
exports: &[X86DocSymbolEntry],
imports: &[String],
submodules: &[String],
) -> String {
let mut html = String::new();
html.push_str("<h2>Exports</h2>\n");
html.push_str(&self.render_symbol_list(exports));
if !imports.is_empty() {
html.push_str("<h2>Imports</h2>\n<ul>\n");
for imp in imports {
html.push_str(&format!(
"<li><code>{}</code></li>\n",
Self::html_escape(imp)
));
}
html.push_str("</ul>\n");
}
if !submodules.is_empty() {
html.push_str("<h2>Submodules</h2>\n<ul>\n");
for sub in submodules {
let link = format!("module_{}.html", sub.replace("::", "_"));
html.push_str(&format!(
"<li><a href=\"{}\">{}</a></li>\n",
Self::html_escape(&link),
Self::html_escape(sub)
));
}
html.push_str("</ul>\n");
}
html
}
pub fn render_hierarchy_page(&mut self) -> String {
let mut html = String::new();
html.push_str("<h1>Class Hierarchy</h1>\n");
let mut ns_groups: BTreeMap<String, Vec<&X86DocSymbolEntry>> = BTreeMap::new();
for sym in &self.symbol_index {
let ns = sym.parent.clone().unwrap_or_else(|| "(global)".to_string());
ns_groups.entry(ns).or_default().push(sym);
}
for (ns, symbols) in &ns_groups {
html.push_str(&format!("<h2>{}</h2>\n", Self::html_escape(ns)));
html.push_str("<ul>\n");
let mut sorted: Vec<_> = symbols.iter().collect();
sorted.sort_by_key(|s| &s.name);
for sym in sorted {
let kind_str = if sym.kind == X86DocSymbolKind::Class
|| sym.kind == X86DocSymbolKind::Struct
{
"class"
} else {
"symbol"
};
html.push_str(&format!(
"<li><a href=\"{}\">{}</a> <span style=\"color:var(--text-tertiary);font-size:0.8em\">({})</span></li>\n",
Self::html_escape(&sym.link),
Self::html_escape(&sym.qualified_name),
kind_str,
));
}
html.push_str("</ul>\n");
}
html
}
fn render_symbol_list(&self, symbols: &[X86DocSymbolEntry]) -> String {
if symbols.is_empty() {
return "<p><em>No symbols.</em></p>\n".to_string();
}
let mut html = String::from("<div class=\"symbol-list\">\n");
for sym in symbols {
html.push_str(&format!(
"<div class=\"symbol-item\" id=\"{}\" data-name=\"{}\" data-kind=\"{}\">\n",
Self::html_escape(&sym.qualified_name),
Self::html_escape(&sym.name),
sym.kind,
));
let display_name = if let Some(ref sig) = sym.signature {
sig.clone()
} else {
sym.qualified_name.clone()
};
html.push_str(&format!(
"<div class=\"signature\"><a href=\"{}\">{}</a></div>\n",
Self::html_escape(&sym.link),
Self::html_escape(&display_name)
));
if let Some(ref brief) = sym.brief {
html.push_str(&format!(
"<div class=\"brief\">{}</div>\n",
Self::html_escape(brief)
));
}
if !sym.parameters.is_empty() {
html.push_str("<div class=\"params\">\n");
for (pname, ptype) in &sym.parameters {
html.push_str("<div class=\"param-row\">");
html.push_str(&format!(
"<span class=\"param-name\">{}</span>",
Self::html_escape(pname)
));
html.push_str(&format!(
"<span class=\"param-desc\">{}</span>",
Self::html_escape(ptype)
));
html.push_str("</div>\n");
}
html.push_str("</div>\n");
}
html.push_str("<div class=\"meta\">\n");
html.push_str(&format!("<span>{}</span>\n", sym.kind));
if let Some(access) = &sym.access {
html.push_str(&format!(" · <span>{}</span>\n", access));
}
if sym.deprecated {
html.push_str("<span class=\"deprecated-badge\">deprecated</span>\n");
}
if let Some(ref since) = sym.since {
html.push_str(&format!(
"<span class=\"since-badge\">since {}</span>\n",
Self::html_escape(since)
));
}
if let Some(ref file) = sym.file {
html.push_str(&format!(" · <span>{}</span>", Self::html_escape(file)));
if let Some(line) = sym.line {
html.push_str(&format!(":{}", line));
}
}
html.push_str("</div>\n");
html.push_str("</div>\n");
}
html.push_str("</div>\n");
html
}
fn build_css(&self) -> String {
let mut css = X86_DOC_DEFAULT_CSS.to_string();
if let Some(ref custom) = self.custom_css {
css.push('\n');
css.push_str(custom);
}
css
}
fn build_js(&self) -> String {
let mut js = String::new();
js.push_str("/* ── Search Index ─────────────────────────────────────────── */\n");
js.push_str("window.X86_SEARCH_INDEX = [\n");
for (i, sym) in self.symbol_index.iter().enumerate() {
if i > 0 {
js.push_str(",\n");
}
js.push_str(" {");
js.push_str(&format!("n:{}", Self::js_string(&sym.qualified_name)));
js.push_str(&format!(",k:{}", Self::js_string(&sym.kind.to_string())));
js.push_str(&format!(
",b:{}",
Self::js_string(sym.brief.as_deref().unwrap_or(""))
));
js.push_str(&format!(",l:{}", Self::js_string(&sym.link)));
js.push_str(&format!(
",p:{}",
Self::js_string(sym.parent.as_deref().unwrap_or(""))
));
js.push('}');
}
js.push_str("\n];\n\n");
js.push_str(X86_DOC_DEFAULT_JS);
js
}
fn js_string(s: &str) -> String {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
format!("\"{}\"", escaped)
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
}
impl Default for X86DocHTML {
fn default() -> Self {
Self::new("Untitled Project", PathBuf::from("doc/html"))
}
}
#[derive(Debug, Clone)]
pub struct X86DocMarkdown {
pub project_name: String,
pub project_version: String,
pub output_file: PathBuf,
pub generate_toc: bool,
pub toc_max_depth: usize,
pub include_api_reference: bool,
pub gfm: bool,
sections: Vec<X86MarkdownSection>,
toc_entries: Vec<X86TOCEntry>,
}
#[derive(Debug, Clone)]
struct X86MarkdownSection {
level: usize,
title: String,
anchor: String,
content: String,
subsections: Vec<X86MarkdownSection>,
}
#[derive(Debug, Clone)]
struct X86TOCEntry {
level: usize,
title: String,
anchor: String,
}
impl X86DocMarkdown {
pub fn new(project_name: &str, output_file: PathBuf) -> Self {
Self {
project_name: project_name.to_string(),
project_version: "0.1.0".to_string(),
output_file,
generate_toc: true,
toc_max_depth: 3,
include_api_reference: true,
gfm: true,
sections: Vec::new(),
toc_entries: Vec::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.project_name.trim().is_empty() {
return Err("Project name cannot be empty".to_string());
}
Ok(())
}
pub fn add_section(&mut self, level: usize, title: &str, content: &str) -> String {
let anchor = Self::make_anchor(title);
let section = X86MarkdownSection {
level,
title: title.to_string(),
anchor: anchor.clone(),
content: content.to_string(),
subsections: Vec::new(),
};
if self.generate_toc && level <= self.toc_max_depth {
self.toc_entries.push(X86TOCEntry {
level,
title: title.to_string(),
anchor: anchor.clone(),
});
}
self.sections.push(section);
anchor
}
pub fn generate_toc_text(&self) -> String {
let mut toc = String::from("## Table of Contents\n\n");
for entry in &self.toc_entries {
let indent = " ".repeat(entry.level.saturating_sub(2));
toc.push_str(&format!(
"{}- [{}](#{})\n",
indent, entry.title, entry.anchor
));
}
toc.push('\n');
toc
}
pub fn generate(&self) -> String {
let mut md = String::new();
md.push_str(&format!("# {} Documentation\n\n", self.project_name));
md.push_str(&format!("**Version:** {}\n\n", self.project_version));
md.push_str("---\n\n");
if self.generate_toc && !self.toc_entries.is_empty() {
md.push_str(&self.generate_toc_text());
md.push_str("---\n\n");
}
for section in &self.sections {
let prefix = "#".repeat(section.level);
md.push_str(&format!("{} {}\n\n", prefix, section.title));
if !section.content.is_empty() {
md.push_str(§ion.content);
md.push_str("\n\n");
}
for sub in §ion.subsections {
let sub_prefix = "#".repeat(sub.level);
md.push_str(&format!("{} {}\n\n", sub_prefix, sub.title));
md.push_str(&sub.content);
md.push_str("\n\n");
}
}
md
}
pub fn generate_api_reference(&self, title: &str, symbols: &[X86DocSymbolEntry]) -> String {
let mut md = String::new();
if !title.is_empty() {
md.push_str(&format!("{}\n\n", title));
}
if symbols.is_empty() {
md.push_str("*No symbols documented.*\n\n");
return md;
}
let mut grouped: BTreeMap<String, Vec<&X86DocSymbolEntry>> = BTreeMap::new();
for sym in symbols {
grouped.entry(sym.kind.to_string()).or_default().push(sym);
}
for (kind, entries) in &grouped {
md.push_str(&format!("### {}\n\n", Self::capitalize(kind)));
for entry in entries {
if let Some(ref sig) = entry.signature {
md.push_str(&format!("#### `{}`\n\n", Self::escape_code(sig)));
} else {
md.push_str(&format!("#### `{}`\n\n", entry.qualified_name));
}
if let Some(ref brief) = entry.brief {
md.push_str(brief);
md.push_str("\n\n");
}
if !entry.parameters.is_empty() {
md.push_str("| Parameter | Type |\n");
md.push_str("|---|---|\n");
for (pname, ptype) in &entry.parameters {
md.push_str(&format!(
"| `{}` | `{}` |\n",
Self::escape_code(pname),
Self::escape_code(ptype)
));
}
md.push('\n');
}
if let Some(ref ret) = entry.return_type {
md.push_str(&format!("**Returns:** `{}`\n\n", Self::escape_code(ret)));
}
if entry.deprecated {
md.push_str("> ⚠️ **Deprecated**\n\n");
}
if let Some(ref since) = entry.since {
md.push_str(&format!("*Since: {}*\n\n", since));
}
if let Some(ref file) = entry.file {
md.push_str(&format!("*Defined in: {}*", file));
if let Some(line) = entry.line {
md.push_str(&format!(":{}", line));
}
md.push_str("\n\n");
}
md.push_str("---\n\n");
}
}
md
}
pub fn generate_symbol_table(&self, symbols: &[X86DocSymbolEntry]) -> String {
let mut md = String::new();
md.push_str("| Name | Kind | Description |\n");
md.push_str("|---|---|---|\n");
for sym in symbols {
md.push_str(&format!(
"| `{}` | {} | {} |\n",
sym.qualified_name,
sym.kind,
sym.brief.as_deref().unwrap_or("—")
));
}
md.push('\n');
md
}
pub fn generate_file_doc(
&self,
filename: &str,
description: Option<&str>,
includes: &[String],
symbols: &[X86DocSymbolEntry],
) -> String {
let mut md = String::new();
md.push_str(&format!("### File: `{}`\n\n", Self::escape_code(filename)));
if let Some(desc) = description {
md.push_str(desc);
md.push_str("\n\n");
}
if !includes.is_empty() {
md.push_str("**Includes:**\n\n");
for inc in includes {
md.push_str(&format!("- `{}`\n", Self::escape_code(inc)));
}
md.push('\n');
}
if !symbols.is_empty() {
md.push_str(&self.generate_symbol_table(symbols));
}
md
}
fn make_anchor(title: &str) -> String {
title
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else if c.is_whitespace() {
'-'
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
fn escape_code(s: &str) -> String {
s.replace('`', "\\`").replace('|', "\\|")
}
fn capitalize(s: &str) -> String {
let mut chars: Vec<char> = s.chars().collect();
if let Some(first) = chars.first_mut() {
*first = first.to_uppercase().next().unwrap_or(*first);
}
chars.into_iter().collect()
}
}
impl Default for X86DocMarkdown {
fn default() -> Self {
Self::new("Untitled Project", PathBuf::from("README.md"))
}
}
#[derive(Debug, Clone)]
pub struct X86DocMan {
pub package: String,
pub section: u32,
pub output_dir: PathBuf,
pub date: String,
pub source: String,
pub use_mandoc: bool,
}
#[derive(Debug, Clone)]
pub struct X86ManPage {
pub name: String,
pub brief: String,
pub synopsis: String,
pub description: String,
pub return_value: String,
pub errors: Vec<(String, String)>,
pub see_also: Vec<String>,
pub notes: Vec<String>,
pub bugs: Vec<String>,
pub examples: Vec<String>,
pub enable_formatting: bool,
pub arch: String,
}
impl X86DocMan {
pub fn new(package: &str, output_dir: PathBuf) -> Self {
Self {
package: package.to_string(),
section: 3,
output_dir,
date: "2025-01-01".to_string(),
source: "LLVM Clang X86".to_string(),
use_mandoc: true,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.package.trim().is_empty() {
return Err("Package name cannot be empty".to_string());
}
Ok(())
}
pub fn generate_man_page(&self, page: &X86ManPage) -> String {
let mut man = String::new();
man.push_str(&format!(
".TH \"{}\" \"{}\" \"{}\" \"{}\" \"{}\"\n",
Self::escape_man(page.name.to_uppercase().as_str()),
self.section,
self.date,
self.source,
self.package
));
man.push_str(".SH NAME\n");
man.push_str(&format!(
"{} \\- {}\n",
Self::escape_man(&page.name),
Self::escape_man(&page.brief)
));
man.push_str(".SH SYNOPSIS\n");
man.push_str(".nf\n");
man.push_str(&Self::escape_man(&page.synopsis));
man.push_str("\n.fi\n");
man.push_str(".SH DESCRIPTION\n");
for line in page.description.lines() {
man.push_str(&format!("{}\n", Self::escape_man(line)));
}
man.push_str(".SH \"RETURN VALUE\"\n");
for line in page.return_value.lines() {
man.push_str(&format!("{}\n", Self::escape_man(line)));
}
if !page.errors.is_empty() {
man.push_str(".SH ERRORS\n");
for (code, desc) in &page.errors {
man.push_str(".TP\n");
man.push_str(&format!(
".B {}\n{}\n",
Self::escape_man(code),
Self::escape_man(desc)
));
}
}
if !page.notes.is_empty() {
man.push_str(".SH NOTES\n");
for note in &page.notes {
man.push_str(&format!("{}\n", Self::escape_man(note)));
}
}
if !page.bugs.is_empty() {
man.push_str(".SH BUGS\n");
for bug in &page.bugs {
man.push_str(&format!("{}\n", Self::escape_man(bug)));
}
}
if !page.examples.is_empty() {
man.push_str(".SH EXAMPLES\n");
for example in &page.examples {
man.push_str(".nf\n");
man.push_str(&Self::escape_man(example));
man.push_str("\n.fi\n");
if page.enable_formatting {
man.push_str(".PP\n");
}
}
}
if !page.see_also.is_empty() {
man.push_str(".SH \"SEE ALSO\"\n");
for (i, see) in page.see_also.iter().enumerate() {
if i > 0 {
man.push_str(", ");
}
man.push_str(&Self::escape_man(see));
}
man.push('\n');
}
if !page.arch.is_empty() {
man.push_str(".SH \"ARCHITECTURE\"\n");
man.push_str(&format!(
"This page documents the {} architecture variant.\n",
Self::escape_man(&page.arch)
));
}
man
}
pub fn generate_man_page_plain(&self, page: &X86ManPage) -> String {
let mut man = String::new();
let width = 72;
man.push_str(&Self::center_text(&page.name.to_uppercase(), width));
man.push_str(&format!("({})\n", self.section));
man.push_str(&format!("{}\n", self.package));
man.push_str(&format!("{}\n\n", self.date));
man.push_str("NAME\n");
man.push_str(&format!(" {} - {}\n\n", page.name, page.brief));
man.push_str("SYNOPSIS\n");
man.push_str(&format!(" {}\n\n", page.synopsis));
man.push_str("DESCRIPTION\n");
for line in page.description.lines() {
man.push_str(&format!(" {}\n", line));
}
man.push('\n');
man.push_str("RETURN VALUE\n");
for line in page.return_value.lines() {
man.push_str(&format!(" {}\n", line));
}
man.push('\n');
if !page.errors.is_empty() {
man.push_str("ERRORS\n");
for (code, desc) in &page.errors {
man.push_str(&format!(" {} {}\n", code, desc));
}
man.push('\n');
}
if !page.see_also.is_empty() {
man.push_str("SEE ALSO\n");
man.push_str(&format!(" {}\n\n", page.see_also.join(", ")));
}
man
}
pub fn generate_for_functions(
&self,
symbols: &[X86DocSymbolEntry],
comments: &HashMap<String, X86DocComment>,
) -> Vec<(String, String)> {
let mut pages = Vec::new();
for sym in symbols {
if sym.kind != X86DocSymbolKind::Function && sym.kind != X86DocSymbolKind::Method {
continue;
}
let comment = comments.get(&sym.qualified_name);
let page = X86ManPage {
name: sym.name.clone(),
brief: sym
.brief
.clone()
.unwrap_or_else(|| "No description available.".to_string()),
synopsis: sym
.signature
.clone()
.unwrap_or_else(|| sym.qualified_name.clone()),
description: comment
.and_then(|c| c.details.clone())
.unwrap_or_else(|| "No detailed description.".to_string()),
return_value: comment
.and_then(|c| c.return_desc.clone())
.unwrap_or_else(|| "See description.".to_string()),
errors: comment
.map(|c| {
c.throws
.iter()
.map(|(t, d)| (t.clone(), d.clone()))
.collect()
})
.unwrap_or_default(),
see_also: comment.map(|c| c.see_also.clone()).unwrap_or_default(),
notes: comment.map(|c| c.notes.clone()).unwrap_or_default(),
bugs: Vec::new(),
examples: comment.map(|c| c.code_blocks.clone()).unwrap_or_default(),
enable_formatting: self.use_mandoc,
arch: X86_DOC_TARGET.to_string(),
};
let man_text = self.generate_man_page(&page);
let file_name = format!("{}.{}", sym.name, self.section);
pages.push((file_name, man_text));
}
pages
}
fn escape_man(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('-', "\\-")
.replace('\'', "\\(aq")
}
fn center_text(text: &str, width: usize) -> String {
if text.len() >= width {
return text.to_string();
}
let padding = (width - text.len()) / 2;
format!("{}{}", " ".repeat(padding), text)
}
}
impl Default for X86DocMan {
fn default() -> Self {
Self::new("llvm-doc", PathBuf::from("doc/man"))
}
}
#[derive(Debug, Clone)]
pub struct X86DocXML {
pub project_name: String,
pub output_dir: PathBuf,
pub doxygen_version: String,
pub pretty_print: bool,
pub include_source_refs: bool,
compounds: Vec<X86XMLCompound>,
compound_defs: HashMap<String, X86XMLCompoundDef>,
}
#[derive(Debug, Clone)]
struct X86XMLCompound {
refid: String,
kind: String,
name: String,
}
#[derive(Debug, Clone)]
struct X86XMLCompoundDef {
refid: String,
kind: String,
xml_content: String,
}
impl X86DocXML {
pub fn new(project_name: &str, output_dir: PathBuf) -> Self {
Self {
project_name: project_name.to_string(),
output_dir,
doxygen_version: "1.9.0".to_string(),
pretty_print: true,
include_source_refs: true,
compounds: Vec::new(),
compound_defs: HashMap::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.project_name.trim().is_empty() {
return Err("Project name cannot be empty".to_string());
}
Ok(())
}
pub fn add_compound(&mut self, refid: &str, kind: &str, name: &str, xml: &str) {
self.compounds.push(X86XMLCompound {
refid: refid.to_string(),
kind: kind.to_string(),
name: name.to_string(),
});
self.compound_defs.insert(
refid.to_string(),
X86XMLCompoundDef {
refid: refid.to_string(),
kind: kind.to_string(),
xml_content: xml.to_string(),
},
);
}
pub fn generate_index_xml(&self) -> String {
let indent = if self.pretty_print { " " } else { "" };
let nl = if self.pretty_print { "\n" } else { "" };
let mut xml = String::new();
xml.push_str(&format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{n}",
n = nl
));
xml.push_str(&format!(
"<doxygenindex version=\"{v}\"{nl}>",
v = Self::xml_escape(&self.doxygen_version),
nl = if self.pretty_print { ">\n" } else { ">" }
));
for c in &self.compounds {
if self.pretty_print {
xml.push_str(&format!(
"{i}<compound refid=\"{ref}\" kind=\"{kind}\"><name>{name}</name></compound>\n",
i = indent,
ref = Self::xml_escape(&c.refid),
kind = Self::xml_escape(&c.kind),
name = Self::xml_escape(&c.name)
));
} else {
xml.push_str(&format!(
"<compound refid=\"{ref}\" kind=\"{kind}\"><name>{name}</name></compound>",
ref = Self::xml_escape(&c.refid),
kind = Self::xml_escape(&c.kind),
name = Self::xml_escape(&c.name)
));
}
}
xml.push_str("</doxygenindex>");
xml
}
pub fn generate_class_xml(
&self,
refid: &str,
name: &str,
kind: &str,
brief: Option<&str>,
details: Option<&str>,
members: &[(String, String, Option<String>)],
methods: &[(String, String, Option<String>)],
bases: &[String],
) -> String {
let indent = if self.pretty_print { " " } else { "" };
let nl = if self.pretty_print { "\n" } else { "" };
let mut xml = String::new();
xml.push_str(&format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{n}",
n = nl
));
xml.push_str(&format!(
"<doxygen><compounddef id=\"{ref}\" kind=\"{kind}\">{nl}",
ref = Self::xml_escape(refid),
kind = Self::xml_escape(kind),
nl = nl
));
xml.push_str(&format!(
"{i}<compoundname>{n}</compoundname>{nl}",
i = indent,
n = Self::xml_escape(name),
nl = nl
));
xml.push_str(&format!(
"{i}<title>{n}</title>{nl}",
i = indent,
n = Self::xml_escape(name),
nl = nl
));
if let Some(b) = brief {
xml.push_str(&format!(
"{i}<briefdescription>{nl}{i}{i}<para>{b}</para>{nl}{i}</briefdescription>{nl}",
i = indent,
nl = nl,
b = Self::xml_escape(b)
));
}
if let Some(d) = details {
xml.push_str(&format!(
"{i}<detaileddescription>{nl}{i}{i}<para>{d}</para>{nl}{i}</detaileddescription>{nl}",
i = indent,
nl = nl,
d = Self::xml_escape(d)
));
}
if !bases.is_empty() {
xml.push_str(&format!("{i}<basecompoundref>{nl}", i = indent));
for base in bases {
xml.push_str(&format!(
"{i}{i}<base>{b}</base>{nl}",
i = indent,
b = Self::xml_escape(base),
nl = nl
));
}
xml.push_str(&format!("{i}</basecompoundref>{nl}", i = indent));
}
if !members.is_empty() {
for (mtype, mname, mdesc) in members {
xml.push_str(&format!(
"{i}<sectiondef kind=\"{mt}\">{nl}",
i = indent,
mt = Self::xml_escape(mtype),
nl = nl
));
xml.push_str(&format!(
"{i}{i}<memberdef kind=\"variable\">{nl}",
i = indent,
nl = nl
));
xml.push_str(&format!(
"{i}{i}{i}<name>{n}</name>{nl}",
i = indent,
n = Self::xml_escape(mname),
nl = nl
));
if let Some(desc) = mdesc {
xml.push_str(&format!(
"{i}{i}{i}<briefdescription><para>{d}</para></briefdescription>{nl}",
i = indent,
d = Self::xml_escape(desc),
nl = nl
));
}
xml.push_str(&format!(
"{i}{i}</memberdef>{nl}{i}</sectiondef>{nl}",
i = indent,
nl = nl
));
}
}
if !methods.is_empty() {
for (mtype, mname, mdesc) in methods {
xml.push_str(&format!(
"{i}<sectiondef kind=\"{mt}\">{nl}",
i = indent,
mt = Self::xml_escape(mtype),
nl = nl
));
xml.push_str(&format!(
"{i}{i}<memberdef kind=\"function\">{nl}",
i = indent,
nl = nl
));
xml.push_str(&format!(
"{i}{i}{i}<name>{n}</name>{nl}",
i = indent,
n = Self::xml_escape(mname),
nl = nl
));
if let Some(desc) = mdesc {
xml.push_str(&format!(
"{i}{i}{i}<briefdescription><para>{d}</para></briefdescription>{nl}",
i = indent,
d = Self::xml_escape(desc),
nl = nl
));
}
xml.push_str(&format!(
"{i}{i}</memberdef>{nl}{i}</sectiondef>{nl}",
i = indent,
nl = nl
));
}
}
xml.push_str(&format!("</compounddef></doxygen>"));
xml
}
pub fn generate_function_xml(
&self,
refid: &str,
name: &str,
return_type: &str,
params: &[(String, String, Option<String>)],
brief: Option<&str>,
details: Option<&str>,
) -> String {
let indent = if self.pretty_print { " " } else { "" };
let nl = if self.pretty_print { "\n" } else { "" };
let mut xml = String::new();
xml.push_str(&format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{n}",
n = nl
));
xml.push_str(&format!(
"<doxygen><compounddef id=\"{ref}\" kind=\"function\">{nl}",
ref = Self::xml_escape(refid),
nl = nl
));
xml.push_str(&format!(
"{i}<compoundname>{n}</compoundname>{nl}",
i = indent,
n = Self::xml_escape(name),
nl = nl
));
xml.push_str(&format!(
"{i}<type>{t}</type>{nl}",
i = indent,
t = Self::xml_escape(return_type),
nl = nl
));
if let Some(b) = brief {
xml.push_str(&format!(
"{i}<briefdescription><para>{b}</para></briefdescription>{nl}",
i = indent,
b = Self::xml_escape(b),
nl = nl
));
}
if let Some(d) = details {
xml.push_str(&format!(
"{i}<detaileddescription><para>{d}</para></detaileddescription>{nl}",
i = indent,
d = Self::xml_escape(d),
nl = nl
));
}
for (pname, ptype, pdesc) in params {
xml.push_str(&format!(
"{i}<param>{nl}{i}{i}<type>{t}</type>{nl}{i}{i}<declname>{n}</declname>{nl}",
i = indent,
t = Self::xml_escape(ptype),
n = Self::xml_escape(pname),
nl = nl
));
if let Some(desc) = pdesc {
xml.push_str(&format!(
"{i}{i}<parameterdescription><para>{d}</para></parameterdescription>{nl}",
i = indent,
d = Self::xml_escape(desc),
nl = nl
));
}
xml.push_str(&format!("{i}</param>{nl}", i = indent));
}
xml.push_str(&format!("</compounddef></doxygen>"));
xml
}
pub fn generate_namespace_xml(
&self,
refid: &str,
name: &str,
brief: Option<&str>,
contents: &[(String, String)],
) -> String {
let indent = if self.pretty_print { " " } else { "" };
let nl = if self.pretty_print { "\n" } else { "" };
let mut xml = String::new();
xml.push_str(&format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{n}",
n = nl
));
xml.push_str(&format!(
"<doxygen><compounddef id=\"{ref}\" kind=\"namespace\">{nl}",
ref = Self::xml_escape(refid),
nl = nl
));
xml.push_str(&format!(
"{i}<compoundname>{n}</compoundname>{nl}",
i = indent,
n = Self::xml_escape(name),
nl = nl
));
if let Some(b) = brief {
xml.push_str(&format!(
"{i}<briefdescription><para>{b}</para></briefdescription>{nl}",
i = indent,
b = Self::xml_escape(b),
nl = nl
));
}
for (kind, ref_name) in contents {
xml.push_str(&format!(
"{i}<innerclass refid=\"{r}\">{n}</innerclass>{nl}",
i = indent,
r = Self::xml_escape(ref_name),
n = Self::xml_escape(kind),
nl = nl
));
}
xml.push_str(&format!("</compounddef></doxygen>"));
xml
}
pub fn generate(&self) -> Vec<(PathBuf, String)> {
let mut output = Vec::new();
output.push((PathBuf::from("index.xml"), self.generate_index_xml()));
for (refid, def) in &self.compound_defs {
let file_name = format!("{}.xml", refid);
output.push((PathBuf::from(file_name), def.xml_content.clone()));
}
output
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
}
impl Default for X86DocXML {
fn default() -> Self {
Self::new("Untitled", PathBuf::from("doc/xml"))
}
}
#[derive(Debug, Clone)]
pub struct X86DocCoverage {
pub enabled: bool,
pub warn_undocumented: bool,
pub warn_missing_param_doc: bool,
pub warn_missing_return_doc: bool,
pub min_coverage_pct: f64,
file_coverage: HashMap<String, X86FileCoverage>,
namespace_coverage: HashMap<String, X86ScopeCoverage>,
class_coverage: HashMap<String, X86ScopeCoverage>,
total_symbols: usize,
documented_symbols: usize,
warnings: Vec<X86DocWarning>,
warning_counts: HashMap<String, usize>,
}
#[derive(Debug, Clone, Default)]
pub struct X86FileCoverage {
pub file_path: String,
pub total_symbols: usize,
pub documented: usize,
pub undocumented: usize,
pub missing_param_doc: usize,
pub missing_return_doc: usize,
pub undocumented_names: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct X86ScopeCoverage {
pub name: String,
pub total_symbols: usize,
pub documented: usize,
pub coverage_pct: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum X86DocWarning {
Undocumented,
MissingBrief,
MissingParamDoc(String),
MissingReturnDoc,
MissingTParamDoc(String),
DeprecatedSymbol,
UnknownParam(String, String),
LowCoverage(String, f64),
}
impl fmt::Display for X86DocWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Undocumented => write!(f, "symbol is not documented"),
Self::MissingBrief => write!(f, "missing brief description"),
Self::MissingParamDoc(name) => {
write!(f, "missing documentation for parameter '{}'", name)
}
Self::MissingReturnDoc => write!(f, "missing return value documentation"),
Self::MissingTParamDoc(name) => {
write!(f, "missing documentation for template parameter '{}'", name)
}
Self::DeprecatedSymbol => write!(f, "symbol is deprecated"),
Self::UnknownParam(sym, param) => {
write!(
f,
"unknown parameter '{}' referenced in documentation of '{}'",
param, sym
)
}
Self::LowCoverage(scope, pct) => {
write!(
f,
"documentation coverage for '{}' is {:.1}% (minimum: 70%)",
scope, pct
)
}
}
}
}
impl X86DocCoverage {
pub fn new() -> Self {
Self {
enabled: true,
warn_undocumented: true,
warn_missing_param_doc: true,
warn_missing_return_doc: true,
min_coverage_pct: 70.0,
file_coverage: HashMap::new(),
namespace_coverage: HashMap::new(),
class_coverage: HashMap::new(),
total_symbols: 0,
documented_symbols: 0,
warnings: Vec::new(),
warning_counts: HashMap::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.min_coverage_pct < 0.0 || self.min_coverage_pct > 100.0 {
return Err("min_coverage_pct must be between 0 and 100".to_string());
}
Ok(())
}
pub fn record_symbol(
&mut self,
qualified_name: &str,
kind: X86DocSymbolKind,
file: Option<&str>,
namespace: Option<&str>,
class: Option<&str>,
comment: Option<&X86DocComment>,
parameters: &[String],
) {
self.total_symbols += 1;
let file_key = file.unwrap_or("(unknown)").to_string();
let file_entry = self
.file_coverage
.entry(file_key.clone())
.or_insert_with(|| X86FileCoverage {
file_path: file_key.clone(),
..Default::default()
});
file_entry.total_symbols += 1;
if let Some(c) = comment {
if !c.is_empty {
file_entry.documented += 1;
self.documented_symbols += 1;
let param_not_docced: Vec<String> = parameters
.iter()
.filter(|p| !c.params.iter().any(|(n, _)| n == *p))
.cloned()
.collect();
for p in ¶m_not_docced {
if self.warn_missing_param_doc {
let warning = X86DocWarning::MissingParamDoc(p.clone());
self.warnings.push(warning);
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
file_entry.missing_param_doc += 1;
}
for (doc_param, _) in &c.params {
if !parameters.contains(doc_param) {
let warning = X86DocWarning::UnknownParam(
qualified_name.to_string(),
doc_param.clone(),
);
self.warnings.push(warning);
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
}
if c.return_desc.is_none() && self.warn_missing_return_doc {
if kind == X86DocSymbolKind::Function || kind == X86DocSymbolKind::Method {
let warning = X86DocWarning::MissingReturnDoc;
self.warnings.push(warning);
file_entry.missing_return_doc += 1;
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
}
if c.brief.is_none() {
let warning = X86DocWarning::MissingBrief;
self.warnings.push(warning);
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
if c.deprecated {
let warning = X86DocWarning::DeprecatedSymbol;
self.warnings.push(warning);
}
} else {
file_entry.undocumented += 1;
file_entry
.undocumented_names
.push(qualified_name.to_string());
if self.warn_undocumented {
let warning = X86DocWarning::Undocumented;
self.warnings.push(warning);
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
}
} else {
file_entry.undocumented += 1;
file_entry
.undocumented_names
.push(qualified_name.to_string());
if self.warn_undocumented {
let warning = X86DocWarning::Undocumented;
self.warnings.push(warning);
*self.warning_counts.entry(file_key.clone()).or_default() += 1;
}
}
if let Some(ns) = namespace {
let ns_entry = self
.namespace_coverage
.entry(ns.to_string())
.or_insert_with(|| X86ScopeCoverage {
name: ns.to_string(),
..Default::default()
});
ns_entry.total_symbols += 1;
if comment.is_some_and(|c| !c.is_empty) {
ns_entry.documented += 1;
}
ns_entry.coverage_pct = Self::calc_pct(ns_entry.documented, ns_entry.total_symbols);
}
if let Some(cls) = class {
let cls_entry = self
.class_coverage
.entry(cls.to_string())
.or_insert_with(|| X86ScopeCoverage {
name: cls.to_string(),
..Default::default()
});
cls_entry.total_symbols += 1;
if comment.is_some_and(|c| !c.is_empty) {
cls_entry.documented += 1;
}
cls_entry.coverage_pct = Self::calc_pct(cls_entry.documented, cls_entry.total_symbols);
}
}
pub fn overall_coverage_pct(&self) -> f64 {
Self::calc_pct(self.documented_symbols, self.total_symbols)
}
pub fn file_coverage_stats(&self) -> Vec<&X86FileCoverage> {
let mut stats: Vec<&X86FileCoverage> = self.file_coverage.values().collect();
stats.sort_by(|a, b| a.file_path.cmp(&b.file_path));
stats
}
pub fn namespace_coverage_stats(&self) -> Vec<&X86ScopeCoverage> {
let mut stats: Vec<&X86ScopeCoverage> = self.namespace_coverage.values().collect();
stats.sort_by(|a, b| a.name.cmp(&b.name));
stats
}
pub fn class_coverage_stats(&self) -> Vec<&X86ScopeCoverage> {
let mut stats: Vec<&X86ScopeCoverage> = self.class_coverage.values().collect();
stats.sort_by(|a, b| a.name.cmp(&b.name));
stats
}
pub fn get_warnings(&self) -> &[X86DocWarning] {
&self.warnings
}
pub fn warning_count_by_file(&self) -> &HashMap<String, usize> {
&self.warning_counts
}
pub fn generate_report(&self) -> String {
let mut report = String::new();
report.push_str("═══ Documentation Coverage Report ═══\n\n");
report.push_str(&format!(
"Overall Coverage: {:.1}% ({}/{})\n\n",
self.overall_coverage_pct(),
self.documented_symbols,
self.total_symbols
));
report.push_str("Per-File Coverage:\n");
report.push_str(&format!(
" {:<50} {:>6} {:>6} {:>6} {:>8}\n",
"File", "Total", "Doc'd", "Undoc", "Coverage"
));
report.push_str(&format!(
" {:-<50} {:-<6} {:-<6} {:-<6} {:-<8}\n",
"", "", "", "", ""
));
for fc in self.file_coverage_stats() {
let pct = Self::calc_pct(fc.documented, fc.total_symbols);
report.push_str(&format!(
" {:<50} {:>6} {:>6} {:>6} {:>7.1}%\n",
fc.file_path, fc.total_symbols, fc.documented, fc.undocumented, pct
));
}
report.push_str("\nPer-Namespace Coverage:\n");
for ns in self.namespace_coverage_stats() {
report.push_str(&format!(
" {}: {:.1}% ({}/{})\n",
ns.name, ns.coverage_pct, ns.documented, ns.total_symbols
));
}
report.push_str("\nPer-Class Coverage:\n");
for cls in self.class_coverage_stats() {
report.push_str(&format!(
" {}: {:.1}% ({}/{})\n",
cls.name, cls.coverage_pct, cls.documented, cls.total_symbols
));
}
let warn_count = self.warnings.len();
report.push_str(&format!("\nDocumentation Warnings: {}\n", warn_count));
if warn_count > 0 {
for warning in &self.warnings {
report.push_str(&format!(" -Wdocumentation: {}\n", warning));
}
}
if !self.warning_counts.is_empty() {
report.push_str("\nWarnings Per File:\n");
for (file, count) in &self.warning_counts {
report.push_str(&format!(" {}: {} warnings\n", file, count));
}
}
report
}
pub fn reset(&mut self) {
self.file_coverage.clear();
self.namespace_coverage.clear();
self.class_coverage.clear();
self.total_symbols = 0;
self.documented_symbols = 0;
self.warnings.clear();
self.warning_counts.clear();
}
fn calc_pct(numer: usize, denom: usize) -> f64 {
if denom == 0 {
100.0
} else {
(numer as f64 / denom as f64) * 100.0
}
}
}
impl Default for X86DocCoverage {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct X86DocGenerator {
pub html: X86DocHTML,
pub markdown: X86DocMarkdown,
pub man: X86DocMan,
pub xml: X86DocXML,
pub coverage: X86DocCoverage,
pub enable_html: bool,
pub enable_markdown: bool,
pub enable_man: bool,
pub enable_xml: bool,
pub enable_coverage: bool,
pub arch: String,
pub validate_output: bool,
}
impl X86DocGenerator {
pub fn new(project_name: &str, output_dir: PathBuf) -> Self {
Self {
html: X86DocHTML::new(project_name, output_dir.join("html")),
markdown: X86DocMarkdown::new(project_name, output_dir.join("markdown/README.md")),
man: X86DocMan::new(project_name, output_dir.join("man")),
xml: X86DocXML::new(project_name, output_dir.join("xml")),
coverage: X86DocCoverage::new(),
enable_html: true,
enable_markdown: true,
enable_man: true,
enable_xml: true,
enable_coverage: true,
arch: X86_DOC_TARGET.to_string(),
validate_output: true,
}
}
pub fn new_x86_32(project_name: &str, output_dir: PathBuf) -> Self {
let mut r#gen = Self::new(project_name, output_dir);
r#gen.arch = "i386-unknown-linux-gnu".to_string();
r#gen
}
pub fn validate(&self) -> Result<Vec<String>, String> {
let mut errors = Vec::new();
if self.enable_html {
if let Err(e) = self.html.validate() {
errors.push(format!("HTML: {}", e));
}
}
if self.enable_markdown {
if let Err(e) = self.markdown.validate() {
errors.push(format!("Markdown: {}", e));
}
}
if self.enable_man {
if let Err(e) = self.man.validate() {
errors.push(format!("Man: {}", e));
}
}
if self.enable_xml {
if let Err(e) = self.xml.validate() {
errors.push(format!("XML: {}", e));
}
}
if self.enable_coverage {
if let Err(e) = self.coverage.validate() {
errors.push(format!("Coverage: {}", e));
}
}
if errors.is_empty() {
Ok(Vec::new())
} else {
Err(errors.join("; "))
}
}
pub fn add_class(
&mut self,
name: &str,
qualified_name: &str,
kind: X86DocSymbolKind,
brief: Option<&str>,
members: &[X86DocSymbolEntry],
methods: &[X86DocSymbolEntry],
bases: &[String],
namespace: Option<&str>,
comment: Option<&X86DocComment>,
) {
if self.enable_coverage {
for m in members {
self.coverage.record_symbol(
&m.qualified_name,
m.kind,
m.file.as_deref(),
namespace,
Some(name),
comment,
&[],
);
}
for m in methods {
let param_names: Vec<String> =
m.parameters.iter().map(|(n, _)| n.clone()).collect();
self.coverage.record_symbol(
&m.qualified_name,
m.kind,
m.file.as_deref(),
namespace,
Some(name),
comment,
¶m_names,
);
}
}
if self.enable_html {
self.html.render_class_page(
name,
qualified_name,
kind,
brief,
members,
methods,
bases,
namespace,
);
}
if self.enable_xml {
let refid = format!("class{}", qualified_name.replace("::", "_"));
let xml = self.xml.generate_class_xml(
&refid,
qualified_name,
&kind.to_string(),
brief,
comment.and_then(|c| c.details.as_deref()),
&[],
&[],
bases,
);
self.xml
.add_compound(&refid, &kind.to_string(), qualified_name, &xml);
}
}
pub fn add_function(&mut self, symbol: &X86DocSymbolEntry, comment: Option<&X86DocComment>) {
if self.enable_coverage {
let param_names: Vec<String> =
symbol.parameters.iter().map(|(n, _)| n.clone()).collect();
self.coverage.record_symbol(
&symbol.qualified_name,
symbol.kind,
symbol.file.as_deref(),
symbol.parent.as_deref(),
None,
comment,
¶m_names,
);
}
if self.enable_html {
self.html.render_function_page(
&symbol.name,
symbol
.signature
.as_deref()
.unwrap_or(&symbol.qualified_name),
symbol.return_type.as_deref().unwrap_or("void"),
&symbol.parameters,
symbol.brief.as_deref(),
comment,
);
}
if self.enable_xml {
let refid = format!("func_{}", symbol.qualified_name.replace("::", "_"));
let param_xml: Vec<(String, String, Option<String>)> = symbol
.parameters
.iter()
.map(|(n, t)| (n.clone(), t.clone(), None))
.collect();
let xml = self.xml.generate_function_xml(
&refid,
&symbol.qualified_name,
symbol.return_type.as_deref().unwrap_or("void"),
¶m_xml,
symbol.brief.as_deref(),
comment.and_then(|c| c.details.as_deref()),
);
self.xml
.add_compound(&refid, "function", &symbol.qualified_name, &xml);
}
if self.enable_markdown {
}
}
pub fn generate_all(
&mut self,
all_symbols: &[X86DocSymbolEntry],
comments: &HashMap<String, X86DocComment>,
) -> Result<X86DocOutput, String> {
let mut output = X86DocOutput::default();
if self.validate_output {
let validation = self
.validate()
.map_err(|e| format!("Validation failed: {}", e))?;
output.validation_messages = validation;
}
if self.enable_html {
self.html.build_nav_tree();
}
if self.enable_html {
let html_files = self.html.generate()?;
output.html_files = html_files;
}
if self.enable_markdown {
let api_ref = self
.markdown
.generate_api_reference("## API Reference\n", all_symbols);
self.markdown.add_section(2, "API Reference", &api_ref);
let symbol_table = self.markdown.generate_symbol_table(all_symbols);
self.markdown.add_section(2, "Symbol Index", &symbol_table);
let markdown_content = self.markdown.generate();
output.markdown_content = Some(markdown_content);
}
if self.enable_man {
let man_pages = self.man.generate_for_functions(all_symbols, comments);
output.man_pages = man_pages;
}
if self.enable_xml {
let xml_files = self.xml.generate();
output.xml_files = xml_files;
}
if self.enable_coverage {
output.coverage_report = Some(self.coverage.generate_report());
}
Ok(output)
}
pub fn generate_index(&mut self) -> String {
let mut index = String::new();
index.push_str(&format!("# {} Documentation\n\n", self.html.project_name));
index.push_str(&format!("**Version:** {}\n\n", self.html.project_version));
index.push_str(&format!("**Architecture:** {}\n\n", self.arch));
index.push_str("## Formats\n\n");
index.push_str("- [HTML Documentation](html/index.html)\n");
index.push_str("- [Markdown Documentation](markdown/README.md)\n");
index.push_str("- [Man Pages](man/)\n");
index.push_str("- [XML Documentation](xml/index.xml)\n");
if self.enable_coverage {
let cov_pct = self.coverage.overall_coverage_pct();
index.push_str(&format!("\n## Coverage: {:.1}%\n\n", cov_pct));
}
index
}
}
impl Default for X86DocGenerator {
fn default() -> Self {
Self::new("Untitled Project", PathBuf::from("doc"))
}
}
#[derive(Debug, Default)]
pub struct X86DocOutput {
pub html_files: Vec<(PathBuf, String)>,
pub markdown_content: Option<String>,
pub man_pages: Vec<(String, String)>,
pub xml_files: Vec<(PathBuf, String)>,
pub coverage_report: Option<String>,
pub validation_messages: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct X86DocJSON {
pub project_name: String,
pub project_version: String,
pub output_file: PathBuf,
pub pretty_print: bool,
entries: Vec<X86JSONSymbolEntry>,
doc_entries: HashMap<String, X86JSONDocEntry>,
}
#[derive(Debug, Clone)]
struct X86JSONSymbolEntry {
name: String,
qualified_name: String,
kind: String,
signature: Option<String>,
return_type: Option<String>,
parameters: Vec<X86JSONParamEntry>,
file: Option<String>,
line: Option<usize>,
parent: Option<String>,
access: Option<String>,
template_params: Vec<String>,
deprecated: bool,
since: Option<String>,
brief: Option<String>,
doc_ref: Option<String>,
}
#[derive(Debug, Clone)]
struct X86JSONParamEntry {
name: String,
type_name: String,
description: Option<String>,
}
#[derive(Debug, Clone)]
struct X86JSONDocEntry {
brief: Option<String>,
details: Option<String>,
params: Vec<(String, String)>,
tparams: Vec<(String, String)>,
return_desc: Option<String>,
retvals: Vec<(String, String)>,
notes: Vec<String>,
warnings: Vec<String>,
see_also: Vec<String>,
deprecated: bool,
deprecation_msg: Option<String>,
since: Option<String>,
throws: Vec<(String, String)>,
code_blocks: Vec<String>,
}
impl X86DocJSON {
pub fn new(project_name: &str, output_file: PathBuf) -> Self {
Self {
project_name: project_name.to_string(),
project_version: "0.1.0".to_string(),
output_file,
pretty_print: true,
entries: Vec::new(),
doc_entries: HashMap::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.project_name.trim().is_empty() {
return Err("Project name cannot be empty".to_string());
}
Ok(())
}
pub fn add_symbol(&mut self, symbol: &X86DocSymbolEntry, comment: Option<&X86DocComment>) {
let doc_ref = if let Some(c) = comment {
let ref_id = format!("doc_{}", symbol.qualified_name.replace("::", "_"));
self.doc_entries.insert(
ref_id.clone(),
X86JSONDocEntry {
brief: c.brief.clone(),
details: c.details.clone(),
params: c.params.clone(),
tparams: c.tparams.clone(),
return_desc: c.return_desc.clone(),
retvals: c.retvals.clone(),
notes: c.notes.clone(),
warnings: c.warnings.clone(),
see_also: c.see_also.clone(),
deprecated: c.deprecated,
deprecation_msg: c.deprecation_msg.clone(),
since: c.since.clone(),
throws: c.throws.clone(),
code_blocks: c.code_blocks.clone(),
},
);
Some(ref_id)
} else {
None
};
self.entries.push(X86JSONSymbolEntry {
name: symbol.name.clone(),
qualified_name: symbol.qualified_name.clone(),
kind: symbol.kind.to_string(),
signature: symbol.signature.clone(),
return_type: symbol.return_type.clone(),
parameters: symbol
.parameters
.iter()
.map(|(n, t)| X86JSONParamEntry {
name: n.clone(),
type_name: t.clone(),
description: comment.and_then(|c| {
c.params
.iter()
.find(|(pn, _)| pn == n)
.map(|(_, d)| d.clone())
}),
})
.collect(),
file: symbol.file.clone(),
line: symbol.line,
parent: symbol.parent.clone(),
access: symbol.access.as_ref().map(|a| a.to_string()),
template_params: symbol.template_params.clone(),
deprecated: symbol.deprecated,
since: symbol.since.clone(),
brief: symbol.brief.clone(),
doc_ref,
});
}
pub fn generate(&self) -> String {
let indent = if self.pretty_print { " " } else { "" };
let nl = if self.pretty_print { "\n" } else { "" };
let mut json = String::new();
json.push_str("{");
if self.pretty_print {
json.push('\n');
}
json.push_str(&format!(
"{i}\"metadata\": {{{nl}{i}{i}\"project\": \"{proj}\",{nl}{i}{i}\"version\": \"{ver}\",{nl}{i}{i}\"generator\": \"X86DocJSON\",{nl}{i}{i}\"target\": \"{target}\"{nl}{i}}},{nl}",
i = indent,
nl = nl,
proj = Self::json_escape(&self.project_name),
ver = Self::json_escape(&self.project_version),
target = X86_DOC_TARGET,
));
json.push_str(&format!("{i}\"symbols\": [{nl}", i = indent));
for (idx, entry) in self.entries.iter().enumerate() {
if idx > 0 {
json.push_str(&format!(",{nl}", nl = nl));
}
json.push_str(&format!("{i}{i}{{{nl}", i = indent));
json.push_str(&format!(
"{i}{i}{i}\"name\": \"{n}\",{nl}",
i = indent,
n = Self::json_escape(&entry.name),
nl = nl
));
json.push_str(&format!(
"{i}{i}{i}\"qualified_name\": \"{q}\",{nl}",
i = indent,
q = Self::json_escape(&entry.qualified_name),
nl = nl
));
json.push_str(&format!(
"{i}{i}{i}\"kind\": \"{k}\",{nl}",
i = indent,
k = Self::json_escape(&entry.kind),
nl = nl
));
if let Some(ref sig) = entry.signature {
json.push_str(&format!(
"{i}{i}{i}\"signature\": \"{s}\",{nl}",
i = indent,
s = Self::json_escape(sig),
nl = nl
));
}
if let Some(ref ret) = entry.return_type {
json.push_str(&format!(
"{i}{i}{i}\"return_type\": \"{r}\",{nl}",
i = indent,
r = Self::json_escape(ret),
nl = nl
));
}
if !entry.parameters.is_empty() {
json.push_str(&format!("{i}{i}{i}\"parameters\": [{nl}", i = indent));
for (pi, param) in entry.parameters.iter().enumerate() {
if pi > 0 {
json.push_str(&format!(",{nl}", nl = nl));
}
json.push_str(&format!(
"{i}{i}{i}{i}{{\"name\": \"{n}\", \"type\": \"{t}\"",
i = indent,
n = Self::json_escape(¶m.name),
t = Self::json_escape(¶m.type_name)
));
if let Some(ref desc) = param.description {
if !desc.is_empty() {
json.push_str(&format!(
", \"description\": \"{d}\"",
d = Self::json_escape(desc)
));
}
}
json.push('}');
}
json.push_str(&format!("{nl}{i}{i}{i}],{nl}", nl = nl, i = indent));
}
if let Some(ref f) = entry.file {
json.push_str(&format!(
"{i}{i}{i}\"file\": \"{f}\",{nl}",
i = indent,
f = Self::json_escape(f),
nl = nl
));
}
if let Some(l) = entry.line {
json.push_str(&format!(
"{i}{i}{i}\"line\": {l},{nl}",
i = indent,
l = l,
nl = nl
));
}
if let Some(ref parent) = entry.parent {
json.push_str(&format!(
"{i}{i}{i}\"parent\": \"{p}\",{nl}",
i = indent,
p = Self::json_escape(parent),
nl = nl
));
}
if let Some(ref access) = entry.access {
json.push_str(&format!(
"{i}{i}{i}\"access\": \"{a}\",{nl}",
i = indent,
a = Self::json_escape(access),
nl = nl
));
}
if entry.deprecated {
json.push_str(&format!(
"{i}{i}{i}\"deprecated\": true,{nl}",
i = indent,
nl = nl
));
}
if let Some(ref since) = entry.since {
json.push_str(&format!(
"{i}{i}{i}\"since\": \"{s}\",{nl}",
i = indent,
s = Self::json_escape(since),
nl = nl
));
}
if let Some(ref brief) = entry.brief {
json.push_str(&format!(
"{i}{i}{i}\"brief\": \"{b}\",{nl}",
i = indent,
b = Self::json_escape(brief),
nl = nl
));
}
if let Some(ref doc_ref) = entry.doc_ref {
json.push_str(&format!(
"{i}{i}{i}\"doc_ref\": \"{r}\"",
i = indent,
r = Self::json_escape(doc_ref)
));
} else {
let trimmed = json.trim_end();
if trimmed.ends_with(',') {
json = trimmed[..trimmed.len() - 1].to_string();
}
if self.pretty_print {
json.push('\n');
}
}
json.push_str(&format!("{i}{i}}}", i = indent));
}
json.push_str(&format!("{nl}{i}],{nl}", nl = nl, i = indent));
json.push_str(&format!("{i}\"documentation\": {{{nl}", i = indent));
let mut doc_idx = 0;
for (ref_id, doc) in &self.doc_entries {
if doc_idx > 0 {
json.push_str(&format!(",{nl}", nl = nl));
}
json.push_str(&format!(
"{i}{i}\"{r}\": {{{nl}",
i = indent,
r = Self::json_escape(ref_id),
nl = nl
));
if let Some(ref b) = doc.brief {
json.push_str(&format!(
"{i}{i}{i}\"brief\": \"{b}\",{nl}",
i = indent,
b = Self::json_escape(b),
nl = nl
));
}
if let Some(ref d) = doc.details {
json.push_str(&format!(
"{i}{i}{i}\"details\": \"{d}\",{nl}",
i = indent,
d = Self::json_escape(d),
nl = nl
));
}
if let Some(ref r) = doc.return_desc {
json.push_str(&format!(
"{i}{i}{i}\"return_desc\": \"{r}\",{nl}",
i = indent,
r = Self::json_escape(r),
nl = nl
));
}
if doc.deprecated {
json.push_str(&format!(
"{i}{i}{i}\"deprecated\": true,{nl}",
i = indent,
nl = nl
));
}
if let Some(ref s) = doc.since {
json.push_str(&format!(
"{i}{i}{i}\"since\": \"{s}\"",
i = indent,
s = Self::json_escape(s),
));
}
json.push_str(&format!("{nl}{i}{i}}}", nl = nl, i = indent));
doc_idx += 1;
}
json.push_str(&format!("{nl}{i}}}{nl}}}", nl = nl, i = indent));
json
}
fn json_escape(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
}
impl Default for X86DocJSON {
fn default() -> Self {
Self::new("Untitled", PathBuf::from("doc.json"))
}
}
#[derive(Debug, Clone)]
pub struct X86DocDiff {
pub baseline: HashMap<String, X86DiffEntry>,
pub current: HashMap<String, X86DiffEntry>,
pub include_unchanged: bool,
pub arch: String,
}
#[derive(Debug, Clone)]
pub struct X86DiffEntry {
pub name: String,
pub kind: X86DocSymbolKind,
pub brief_hash: u64,
pub body_hash: u64,
pub is_documented: bool,
}
#[derive(Debug, Clone)]
pub struct X86DiffResult {
pub stats: X86DiffStats,
pub added: Vec<String>,
pub removed: Vec<String>,
pub modified: Vec<X86DiffChange>,
pub unchanged: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct X86DiffStats {
pub added_count: usize,
pub removed_count: usize,
pub modified_count: usize,
pub unchanged_count: usize,
pub baseline_total: usize,
pub current_total: usize,
}
#[derive(Debug, Clone)]
pub struct X86DiffChange {
pub name: String,
pub change_type: X86DiffChangeType,
pub old_brief: Option<String>,
pub new_brief: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86DiffChangeType {
BriefChanged,
BodyChanged,
DocAdded,
DocRemoved,
ParamsChanged,
ReturnChanged,
}
impl fmt::Display for X86DiffChangeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BriefChanged => write!(f, "brief-changed"),
Self::BodyChanged => write!(f, "body-changed"),
Self::DocAdded => write!(f, "doc-added"),
Self::DocRemoved => write!(f, "doc-removed"),
Self::ParamsChanged => write!(f, "params-changed"),
Self::ReturnChanged => write!(f, "return-changed"),
}
}
}
impl X86DocDiff {
pub fn new() -> Self {
Self {
baseline: HashMap::new(),
current: HashMap::new(),
include_unchanged: false,
arch: X86_DOC_TARGET.to_string(),
}
}
pub fn validate(&self) -> Result<(), String> {
Ok(())
}
pub fn load_baseline(
&mut self,
symbols: &[X86DocSymbolEntry],
comments: &HashMap<String, X86DocComment>,
) {
self.baseline = Self::build_entries(symbols, comments);
}
pub fn load_current(
&mut self,
symbols: &[X86DocSymbolEntry],
comments: &HashMap<String, X86DocComment>,
) {
self.current = Self::build_entries(symbols, comments);
}
fn build_entries(
symbols: &[X86DocSymbolEntry],
comments: &HashMap<String, X86DocComment>,
) -> HashMap<String, X86DiffEntry> {
let mut entries = HashMap::new();
for sym in symbols {
let comment = comments.get(&sym.qualified_name);
let is_docced = comment.is_some_and(|c| !c.is_empty);
let brief_hash =
Self::hash_string(comment.and_then(|c| c.brief.as_deref()).unwrap_or(""));
let body = comment
.map(|c| &c.markdown_body)
.cloned()
.unwrap_or_default();
let body_hash = Self::hash_string(&body);
entries.insert(
sym.qualified_name.clone(),
X86DiffEntry {
name: sym.name.clone(),
kind: sym.kind,
brief_hash,
body_hash,
is_documented: is_docced,
},
);
}
entries
}
pub fn compute_diff(&self) -> X86DiffResult {
let mut result = X86DiffResult {
stats: X86DiffStats {
baseline_total: self.baseline.len(),
current_total: self.current.len(),
..Default::default()
},
added: Vec::new(),
removed: Vec::new(),
modified: Vec::new(),
unchanged: Vec::new(),
};
for name in self.current.keys() {
if !self.baseline.contains_key(name) {
result.added.push(name.clone());
result.stats.added_count += 1;
}
}
for name in self.baseline.keys() {
if !self.current.contains_key(name) {
result.removed.push(name.clone());
result.stats.removed_count += 1;
}
}
for (name, entry) in &self.current {
if let Some(old_entry) = self.baseline.get(name) {
let old_docced = old_entry.is_documented;
let new_docced = entry.is_documented;
if old_docced != new_docced {
let change_type = if new_docced {
X86DiffChangeType::DocAdded
} else {
X86DiffChangeType::DocRemoved
};
result.modified.push(X86DiffChange {
name: name.clone(),
change_type,
old_brief: None,
new_brief: None,
});
result.stats.modified_count += 1;
} else if old_entry.brief_hash != entry.brief_hash {
result.modified.push(X86DiffChange {
name: name.clone(),
change_type: X86DiffChangeType::BriefChanged,
old_brief: None,
new_brief: None,
});
result.stats.modified_count += 1;
} else if old_entry.body_hash != entry.body_hash {
result.modified.push(X86DiffChange {
name: name.clone(),
change_type: X86DiffChangeType::BodyChanged,
old_brief: None,
new_brief: None,
});
result.stats.modified_count += 1;
} else {
result.unchanged.push(name.clone());
result.stats.unchanged_count += 1;
}
}
}
result.added.sort();
result.removed.sort();
result
}
pub fn generate_report(&self) -> String {
let diff = self.compute_diff();
let mut report = String::new();
report.push_str("═══ Documentation Diff Report ═══\n\n");
report.push_str(&format!(
"Baseline: {} symbols, Current: {} symbols\n",
diff.stats.baseline_total, diff.stats.current_total
));
report.push_str(&format!(
"Added: {}, Removed: {}, Modified: {}, Unchanged: {}\n\n",
diff.stats.added_count,
diff.stats.removed_count,
diff.stats.modified_count,
diff.stats.unchanged_count
));
if !diff.added.is_empty() {
report.push_str("Added Symbols:\n");
for name in &diff.added {
report.push_str(&format!(" + {}\n", name));
}
report.push('\n');
}
if !diff.removed.is_empty() {
report.push_str("Removed Symbols:\n");
for name in &diff.removed {
report.push_str(&format!(" - {}\n", name));
}
report.push('\n');
}
if !diff.modified.is_empty() {
report.push_str("Modified Symbols:\n");
for change in &diff.modified {
report.push_str(&format!(" ~ {} [{}]\n", change.name, change.change_type));
}
report.push('\n');
}
report
}
fn hash_string(s: &str) -> u64 {
let mut hash: u64 = 5381;
for b in s.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(b as u64);
}
hash
}
}
impl Default for X86DocDiff {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86DocIndexBuilder {
pub arch: String,
pub index_names: bool,
pub index_briefs: bool,
pub index_details: bool,
pub index_params: bool,
pub enable_stemming: bool,
pub remove_stop_words: bool,
entries: Vec<X86IndexEntry>,
inverted_index: HashMap<String, Vec<usize>>,
}
#[derive(Debug, Clone)]
pub struct X86IndexEntry {
pub name: String,
pub kind: String,
pub link: String,
pub tokens: Vec<String>,
pub score: f64,
}
impl X86DocIndexBuilder {
pub fn new() -> Self {
Self {
arch: X86_DOC_TARGET.to_string(),
index_names: true,
index_briefs: true,
index_details: true,
index_params: true,
enable_stemming: true,
remove_stop_words: true,
entries: Vec::new(),
inverted_index: HashMap::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
Ok(())
}
pub fn add_to_index(&mut self, symbol: &X86DocSymbolEntry, comment: Option<&X86DocComment>) {
let mut tokens: Vec<String> = Vec::new();
if self.index_names {
for part in symbol.qualified_name.split("::") {
tokens.push(part.to_lowercase());
}
tokens.push(symbol.name.to_lowercase());
}
if self.index_briefs {
if let Some(ref brief) = symbol.brief {
tokens.extend(self.tokenize(brief));
}
if let Some(c) = comment {
if let Some(ref brief) = c.brief {
tokens.extend(self.tokenize(brief));
}
}
}
if self.index_details {
if let Some(c) = comment {
if let Some(ref details) = c.details {
tokens.extend(self.tokenize(details));
}
tokens.extend(self.tokenize(&c.markdown_body));
}
}
if self.index_params {
for (pname, _) in &symbol.parameters {
tokens.push(pname.to_lowercase());
}
if let Some(c) = comment {
for (pname, _) in &c.params {
tokens.push(pname.to_lowercase());
}
}
}
if self.remove_stop_words {
tokens = tokens.into_iter().filter(|t| !is_stop_word(t)).collect();
}
if self.enable_stemming {
tokens = tokens.into_iter().map(|t| simple_stem(&t)).collect();
}
tokens.sort();
tokens.dedup();
let entry_idx = self.entries.len();
for token in &tokens {
self.inverted_index
.entry(token.clone())
.or_default()
.push(entry_idx);
}
self.entries.push(X86IndexEntry {
name: symbol.qualified_name.clone(),
kind: symbol.kind.to_string(),
link: symbol.link.clone(),
tokens,
score: 0.0,
});
}
pub fn search(&self, query: &str) -> Vec<&X86IndexEntry> {
let query_tokens: Vec<String> = self
.tokenize(query)
.into_iter()
.filter(|t| !self.remove_stop_words || !is_stop_word(t))
.map(|t| {
if self.enable_stemming {
simple_stem(&t)
} else {
t
}
})
.collect();
if query_tokens.is_empty() {
return Vec::new();
}
let mut scored: Vec<(usize, f64)> = Vec::new();
for token in &query_tokens {
if let Some(entry_indices) = self.inverted_index.get(token) {
for &idx in entry_indices {
if let Some((_, score)) = scored.iter_mut().find(|(i, _)| *i == idx) {
*score += 1.0;
} else {
scored.push((idx, 1.0));
}
}
}
}
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored
.into_iter()
.filter_map(|(idx, _)| self.entries.get(idx))
.collect()
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn token_count(&self) -> usize {
self.inverted_index.len()
}
fn tokenize(&self, text: &str) -> Vec<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
}
impl Default for X86DocIndexBuilder {
fn default() -> Self {
Self::new()
}
}
fn is_stop_word(word: &str) -> bool {
matches!(
word,
"a" | "an"
| "the"
| "is"
| "are"
| "was"
| "were"
| "be"
| "been"
| "being"
| "have"
| "has"
| "had"
| "do"
| "does"
| "did"
| "will"
| "would"
| "could"
| "should"
| "may"
| "might"
| "can"
| "shall"
| "this"
| "that"
| "these"
| "those"
| "it"
| "its"
| "of"
| "in"
| "to"
| "for"
| "on"
| "with"
| "at"
| "by"
| "from"
| "as"
| "into"
| "through"
| "during"
| "before"
| "after"
| "above"
| "below"
| "between"
| "and"
| "but"
| "or"
| "nor"
| "not"
| "so"
| "yet"
| "both"
| "either"
| "neither"
| "each"
| "every"
| "all"
| "any"
| "few"
| "more"
| "most"
| "other"
| "some"
| "such"
| "no"
| "only"
| "own"
| "same"
| "than"
| "too"
| "very"
| "just"
| "about"
| "also"
| "if"
| "then"
| "else"
| "when"
| "where"
| "why"
| "how"
| "which"
| "who"
| "whom"
| "what"
)
}
fn simple_stem(word: &str) -> String {
let w = word.to_lowercase();
if w.len() <= 4 {
return w;
}
let suffixes = [
("ing", 3),
("tion", 4),
("sion", 4),
("ment", 4),
("ness", 4),
("able", 4),
("ible", 4),
("ful", 3),
("less", 4),
("ous", 3),
("ive", 3),
("ize", 3),
("ise", 3),
("ed", 2),
("er", 2),
("est", 3),
("ly", 2),
("es", 2),
("s", 1),
];
for (suffix, min_removed) in suffixes.iter() {
if w.ends_with(suffix) && w.len() > suffix.len() + min_removed {
return w[..w.len() - suffix.len()].to_string();
}
}
w
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comment_new_is_empty() {
let c = X86DocComment::new();
assert!(c.is_empty);
assert_eq!(c.kind, X86CommentKind::LineDoc);
}
#[test]
fn test_comment_default_is_empty() {
let c = X86DocComment::default();
assert!(c.is_empty);
}
#[test]
fn test_comment_parse_line_doc_simple() {
let raw = "/// This is a brief description.\n/// More details here.";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
assert_eq!(c.kind, X86CommentKind::LineDoc);
assert!(c.brief.is_some());
assert!(c.brief.unwrap().contains("brief description"));
}
#[test]
fn test_comment_parse_block_doc() {
let raw = "/**\n * Brief description.\n *\n * Detailed description.\n */";
let c = X86DocComment::from_block_doc(raw);
assert!(!c.is_empty);
assert_eq!(c.kind, X86CommentKind::BlockDoc);
}
#[test]
fn test_comment_parse_module_doc() {
let raw = "//! Module-level documentation.";
let c = X86DocComment::from_module_doc(raw);
assert!(!c.is_empty);
assert!(c.is_module_level);
assert_eq!(c.kind, X86CommentKind::ModuleDoc);
}
#[test]
fn test_comment_parse_param() {
let raw = "/// \\param x The x coordinate.\n/// \\param y The y coordinate.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.params.len(), 2);
assert_eq!(c.params[0].0, "x");
assert_eq!(c.params[1].0, "y");
}
#[test]
fn test_comment_parse_param_at_sign() {
let raw = "/// @param x The x coordinate.\n/// @param y The y coordinate.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.params.len(), 2);
}
#[test]
fn test_comment_parse_return() {
let raw = "/// \\return The computed result.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.return_desc.is_some());
assert_eq!(c.return_desc.unwrap(), "The computed result.");
}
#[test]
fn test_comment_parse_return_at() {
let raw = "/// @return The result.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.return_desc.is_some());
}
#[test]
fn test_comment_parse_brief() {
let raw = "/// \\brief A brief summary.\n/// More details.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.brief.is_some());
assert_eq!(c.brief.unwrap(), "A brief summary.");
}
#[test]
fn test_comment_parse_details() {
let raw = "/// \\details This is the detailed description.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.details.is_some());
}
#[test]
fn test_comment_parse_note() {
let raw = "/// \\note This is an important note.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.notes.len(), 1);
assert!(c.notes[0].contains("important note"));
}
#[test]
fn test_comment_parse_warning() {
let raw = "/// \\warning This is dangerous!";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.warnings.len(), 1);
}
#[test]
fn test_comment_parse_see() {
let raw = "/// \\see other_function";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.see_also.len(), 1);
assert_eq!(c.see_also[0], "other_function");
}
#[test]
fn test_comment_parse_deprecated() {
let raw = "/// \\deprecated Use new_function instead.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.deprecated);
assert!(c.deprecation_msg.is_some());
}
#[test]
fn test_comment_parse_since() {
let raw = "/// \\since v1.2.0";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.since.unwrap(), "v1.2.0");
}
#[test]
fn test_comment_parse_throws() {
let raw = "/// \\throws std::runtime_error If something goes wrong.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.throws.len(), 1);
assert_eq!(c.throws[0].0, "std::runtime_error");
}
#[test]
fn test_comment_parse_tparam() {
let raw = "/// \\tparam T The value type.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.tparams.len(), 1);
assert_eq!(c.tparams[0].0, "T");
}
#[test]
fn test_comment_parse_retval() {
let raw = "/// \\retval 0 Success.\n/// \\retval -1 Error.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.retvals.len(), 2);
}
#[test]
fn test_comment_parse_code_block() {
let raw = "/// \\code\n/// int x = 42;\n/// \\endcode\n/// Text after.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.code_blocks.len(), 1);
assert!(c.code_blocks[0].contains("int x = 42"));
}
#[test]
fn test_comment_parse_verbatim_block() {
let raw = "/// \\verbatim\n/// raw text <>&\n/// \\endverbatim";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.verbatim_blocks.len(), 1);
}
#[test]
fn test_comment_parse_markdown_bold() {
let raw = "/// This is **bold** text.";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<strong>"));
}
#[test]
fn test_comment_parse_markdown_italic() {
let raw = "/// This is *italic* text.";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<em>"));
}
#[test]
fn test_comment_parse_markdown_inline_code() {
let raw = "/// Use `std::vector` for storage.";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<code>"));
}
#[test]
fn test_comment_parse_markdown_link() {
let raw = "/// See [the docs](https://example.com) for more.";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.links.len(), 1);
assert_eq!(c.links[0].0, "the docs");
assert_eq!(c.links[0].1, "https://example.com");
}
#[test]
fn test_comment_parse_markdown_link_in_html() {
let raw = "/// See [the docs](https://example.com).";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("href="));
assert!(html.contains("https://example.com"));
}
#[test]
fn test_comment_parse_markdown_heading() {
let raw = "/// # Heading 1\n/// ## Heading 2\n/// Normal text.";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<h1>"));
assert!(html.contains("<h2>"));
}
#[test]
fn test_comment_parse_markdown_list() {
let raw = "/// - Item 1\n/// - Item 2\n/// - Item 3";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<ul>"));
assert!(html.contains("<li>"));
}
#[test]
fn test_comment_parse_markdown_code_fence() {
let raw = "/// ```cpp\n/// int main() { return 0; }\n/// ```";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<pre><code"));
}
#[test]
fn test_comment_empty() {
let raw = "";
let c = X86DocComment::from_line_doc(raw);
assert!(c.is_empty);
}
#[test]
fn test_comment_has_content() {
let c = X86DocComment::from_line_doc("/// Something");
assert!(c.has_content());
}
#[test]
fn test_comment_merge_lines() {
let lines = vec![
"/// Brief description.".to_string(),
"/// More detail.".to_string(),
];
let c = X86DocComment::merge_lines(&lines);
assert!(!c.is_empty);
}
#[test]
fn test_comment_post_doc() {
let raw = "/**< post-declaration comment. */";
let c = X86DocComment::from_post_doc(raw);
assert_eq!(c.kind, X86CommentKind::PostDoc);
}
#[test]
fn test_comment_module_block_doc() {
let raw = "/*! Module-level block doc. */";
let c = X86DocComment::from_module_block_doc(raw);
assert_eq!(c.kind, X86CommentKind::ModuleBlockDoc);
assert!(c.is_module_level);
}
#[test]
fn test_comment_validate_undocumented() {
let c = X86DocComment::new();
let warnings = c.validate();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::Undocumented)));
}
#[test]
fn test_comment_validate_deprecated() {
let raw = "/// \\deprecated";
let c = X86DocComment::from_line_doc(raw);
let warnings = c.validate();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::DeprecatedSymbol)));
}
#[test]
fn test_comment_value() {
let raw = "/// \\version 1.0.0\n/// \\author John Doe\n/// \\date 2025-01-01";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_custom_tags() {
let raw = "/// \\custom_tag some value";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_with_multiple_commands() {
let raw = "/// \\brief Get the value.\n/// \\param key The lookup key.\n/// \\return The value or empty.\n/// \\throws std::out_of_range\n/// \\note Thread-safe.\n/// \\warning May block.\n/// \\see set_value\n/// \\since v2.0\n/// \\deprecated Use get_value_v2 instead.\n/// \\tparam K The key type.\n/// \\retval 0 Found.\n/// \\retval -1 Not found.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.brief.is_some());
assert_eq!(c.params.len(), 1);
assert!(c.return_desc.is_some());
assert_eq!(c.throws.len(), 1);
assert_eq!(c.notes.len(), 1);
assert_eq!(c.warnings.len(), 1);
assert_eq!(c.see_also.len(), 1);
assert!(c.since.is_some());
assert!(c.deprecated);
assert_eq!(c.tparams.len(), 1);
assert_eq!(c.retvals.len(), 2);
}
#[test]
fn test_comment_kind_display() {
assert_eq!(X86CommentKind::LineDoc.to_string(), "line-doc");
assert_eq!(X86CommentKind::BlockDoc.to_string(), "block-doc");
assert_eq!(X86CommentKind::ModuleDoc.to_string(), "module-doc");
assert_eq!(
X86CommentKind::ModuleBlockDoc.to_string(),
"module-block-doc"
);
assert_eq!(X86CommentKind::PostDoc.to_string(), "post-doc");
}
#[test]
fn test_html_new() {
let html = X86DocHTML::new("TestProject", PathBuf::from("doc/html"));
assert_eq!(html.project_name, "TestProject");
assert!(html.generate_search);
assert!(html.dark_mode);
}
#[test]
fn test_html_default() {
let html = X86DocHTML::default();
assert!(html.project_name.len() > 0);
}
#[test]
fn test_html_validate_empty_name() {
let html = X86DocHTML::new("", PathBuf::from("doc"));
assert!(html.validate().is_err());
}
#[test]
fn test_html_validate_good_name() {
let html = X86DocHTML::new("Project", PathBuf::from("doc"));
assert!(html.validate().is_ok());
}
#[test]
fn test_html_add_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let page = X86DocPage {
title: "Test".to_string(),
kind: X86DocPageKind::Class,
output_file: PathBuf::from("test.html"),
breadcrumb: vec![],
body_html: "<p>test</p>".to_string(),
symbols: vec![X86DocSymbolEntry {
qualified_name: "TestClass".to_string(),
name: "TestClass".to_string(),
kind: X86DocSymbolKind::Class,
brief: Some("A test class".to_string()),
file: Some("test.h".to_string()),
line: Some(42),
parent: None,
link: "class_TestClass.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}],
sub_pages: vec![],
};
html.add_page(page);
assert_eq!(html.symbol_index.len(), 1);
assert_eq!(html.pages.len(), 1);
}
#[test]
fn test_html_build_nav_tree() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let sym = X86DocSymbolEntry {
qualified_name: "ns::MyClass".to_string(),
name: "MyClass".to_string(),
kind: X86DocSymbolKind::Class,
brief: None,
file: None,
line: None,
parent: Some("ns".to_string()),
link: "class_ns_MyClass.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
html.symbol_index.push(sym);
html.build_nav_tree();
assert!(!html.nav_tree.roots.is_empty());
}
#[test]
fn test_html_generate_css() {
let html = X86DocHTML::new("P", PathBuf::from("doc"));
let css = html.build_css();
assert!(css.contains("--bg-primary"));
assert!(css.contains("dark"));
}
#[test]
fn test_html_generate_js() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.symbol_index.push(X86DocSymbolEntry {
qualified_name: "foo".to_string(),
name: "foo".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("does stuff".to_string()),
file: None,
line: None,
parent: None,
link: "func_foo.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
});
let js = html.build_js();
assert!(js.contains("X86_SEARCH_INDEX"));
assert!(js.contains("foo"));
}
#[test]
fn test_html_page_kind_display() {
assert_eq!(X86DocPageKind::Index.to_string(), "index");
assert_eq!(X86DocPageKind::Namespace.to_string(), "namespace");
assert_eq!(X86DocPageKind::Class.to_string(), "class");
assert_eq!(X86DocPageKind::Function.to_string(), "function");
assert_eq!(X86DocPageKind::Enum.to_string(), "enum");
assert_eq!(X86DocPageKind::TypeDef.to_string(), "typedef");
assert_eq!(X86DocPageKind::File.to_string(), "file");
assert_eq!(X86DocPageKind::Module.to_string(), "module");
assert_eq!(X86DocPageKind::HierarchicalIndex.to_string(), "hierarchy");
assert_eq!(X86DocPageKind::Search.to_string(), "search");
assert_eq!(X86DocPageKind::Topic.to_string(), "topic");
}
#[test]
fn test_html_symbol_kind_display() {
assert_eq!(X86DocSymbolKind::Namespace.to_string(), "namespace");
assert_eq!(X86DocSymbolKind::Class.to_string(), "class");
assert_eq!(X86DocSymbolKind::Function.to_string(), "function");
assert_eq!(X86DocSymbolKind::Method.to_string(), "method");
assert_eq!(X86DocSymbolKind::Constructor.to_string(), "constructor");
}
#[test]
fn test_html_access_display() {
assert_eq!(X86DocAccess::Public.to_string(), "public");
assert_eq!(X86DocAccess::Protected.to_string(), "protected");
assert_eq!(X86DocAccess::Private.to_string(), "private");
}
#[test]
fn test_html_render_class_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let members = vec![];
let methods = vec![];
let bases = vec!["BaseClass".to_string()];
let result = html.render_class_page(
"MyClass",
"ns::MyClass",
X86DocSymbolKind::Class,
Some("A test class"),
&members,
&methods,
&bases,
Some("ns"),
);
assert!(result.contains("MyClass"));
assert!(result.contains("BaseClass"));
}
#[test]
fn test_html_render_function_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let params = vec![("x".to_string(), "int".to_string())];
let result = html.render_function_page(
"my_func",
"int my_func(int x)",
"int",
¶ms,
Some("Does something"),
None,
);
assert!(result.contains("my_func"));
assert!(result.contains("int"));
}
#[test]
fn test_html_render_namespace_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let contents = vec![];
let nested = vec!["ns::inner".to_string()];
let result = html.render_namespace_page("ns", &contents, &nested, Some("Root namespace"));
assert!(result.contains("ns"));
}
#[test]
fn test_html_render_file_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let includes = vec!["<stdio.h>".to_string()];
let result = html.render_file_page("file.c", &includes, &[], &[], &[]);
assert!(result.contains("stdio.h"));
}
#[test]
fn test_html_render_module_page() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let result = html.render_module_page(
"mymod",
&[],
&["<cstdint>".to_string()],
&["submod".to_string()],
);
assert!(result.contains("cstdint"));
assert!(result.contains("submod"));
}
#[test]
fn test_html_render_hierarchy() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.symbol_index.push(X86DocSymbolEntry {
qualified_name: "Base".to_string(),
name: "Base".to_string(),
kind: X86DocSymbolKind::Class,
brief: None,
file: None,
line: None,
parent: None,
link: "base.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
});
let result = html.render_hierarchy_page();
assert!(result.contains("Base"));
}
#[test]
fn test_html_generate_produces_files() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.pages.push(X86DocPage {
title: "TestPage".to_string(),
kind: X86DocPageKind::Class,
output_file: PathBuf::from("test.html"),
breadcrumb: vec![("Home".to_string(), "index.html".to_string())],
body_html: "<p>content</p>".to_string(),
symbols: vec![],
sub_pages: vec![],
});
let result = html.generate();
assert!(result.is_ok());
let files = result.unwrap();
assert!(files.len() >= 3); }
#[test]
fn test_markdown_new() {
let md = X86DocMarkdown::new("P", PathBuf::from("README.md"));
assert_eq!(md.project_name, "P");
assert!(md.generate_toc);
}
#[test]
fn test_markdown_validate() {
let md = X86DocMarkdown::new("", PathBuf::from("f"));
assert!(md.validate().is_err());
let md2 = X86DocMarkdown::new("P", PathBuf::from("f"));
assert!(md2.validate().is_ok());
}
#[test]
fn test_markdown_add_section() {
let mut md = X86DocMarkdown::new("P", PathBuf::from("f"));
let anchor = md.add_section(2, "API Reference", "Content here.");
assert!(!anchor.is_empty());
assert_eq!(md.toc_entries.len(), 1);
}
#[test]
fn test_markdown_generate_toc() {
let mut md = X86DocMarkdown::new("P", PathBuf::from("f"));
md.add_section(2, "Introduction", "");
md.add_section(2, "API Reference", "");
md.add_section(3, "Functions", "");
let toc = md.generate_toc_text();
assert!(toc.contains("Introduction"));
assert!(toc.contains("API Reference"));
assert!(toc.contains("Functions"));
}
#[test]
fn test_markdown_generate() {
let mut md = X86DocMarkdown::new("Test", PathBuf::from("f"));
md.add_section(2, "Section", "Content.");
let output = md.generate();
assert!(output.contains("# Test Documentation"));
assert!(output.contains("## Section"));
assert!(output.contains("Content."));
}
#[test]
fn test_markdown_api_reference() {
let md = X86DocMarkdown::new("P", PathBuf::from("f"));
let symbols = vec![X86DocSymbolEntry {
qualified_name: "my_func".to_string(),
name: "my_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Does something".to_string()),
file: Some("file.c".to_string()),
line: Some(10),
parent: None,
link: "func.html".to_string(),
access: None,
template_params: vec![],
signature: Some("int my_func(int x)".to_string()),
return_type: Some("int".to_string()),
parameters: vec![("x".to_string(), "int".to_string())],
deprecated: false,
since: Some("v1.0".to_string()),
}];
let api_ref = md.generate_api_reference("## API", &symbols);
assert!(api_ref.contains("my_func"));
assert!(api_ref.contains("int"));
assert!(api_ref.contains("v1.0"));
}
#[test]
fn test_markdown_symbol_table() {
let md = X86DocMarkdown::new("P", PathBuf::from("f"));
let symbols = vec![X86DocSymbolEntry {
qualified_name: "foo".to_string(),
name: "foo".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("desc".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let table = md.generate_symbol_table(&symbols);
assert!(table.contains("foo"));
assert!(table.contains("desc"));
}
#[test]
fn test_markdown_file_doc() {
let md = X86DocMarkdown::new("P", PathBuf::from("f"));
let symbols = vec![];
let result = md.generate_file_doc(
"file.h",
Some("Header file."),
&["<stdio.h>".to_string()],
&symbols,
);
assert!(result.contains("file.h"));
assert!(result.contains("stdio.h"));
}
#[test]
fn test_markdown_deprecated_symbol() {
let md = X86DocMarkdown::new("P", PathBuf::from("f"));
let symbols = vec![X86DocSymbolEntry {
qualified_name: "old_fn".to_string(),
name: "old_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: true,
since: None,
}];
let result = md.generate_api_reference("## API", &symbols);
assert!(result.contains("Deprecated"));
}
#[test]
fn test_man_new() {
let man = X86DocMan::new("libfoo", PathBuf::from("doc/man"));
assert_eq!(man.package, "libfoo");
assert_eq!(man.section, 3);
}
#[test]
fn test_man_validate() {
let man = X86DocMan::new("", PathBuf::from("d"));
assert!(man.validate().is_err());
let man2 = X86DocMan::new("pkg", PathBuf::from("d"));
assert!(man2.validate().is_ok());
}
#[test]
fn test_man_generate_page() {
let man = X86DocMan::new("libfoo", PathBuf::from("d"));
let page = X86ManPage {
name: "foo_init".to_string(),
brief: "Initialize the foo library".to_string(),
synopsis: "int foo_init(int flags);".to_string(),
description: "Initializes the foo library for use.".to_string(),
return_value: "Returns 0 on success.".to_string(),
errors: vec![("EINVAL".to_string(), "Invalid flags".to_string())],
see_also: vec!["foo_cleanup(3)".to_string()],
notes: vec!["Thread-safe.".to_string()],
bugs: vec![],
examples: vec!["int r = foo_init(0);".to_string()],
enable_formatting: true,
arch: "x86_64".to_string(),
};
let man_text = man.generate_man_page(&page);
assert!(man_text.contains(".TH"));
assert!(man_text.contains("foo_init"));
assert!(man_text.contains("SYNOPSIS"));
assert!(man_text.contains("RETURN VALUE"));
assert!(man_text.contains("EINVAL"));
}
#[test]
fn test_man_generate_plain() {
let man = X86DocMan::new("libfoo", PathBuf::from("d"));
let page = X86ManPage {
name: "bar".to_string(),
brief: "Does bar".to_string(),
synopsis: "void bar();".to_string(),
description: "Bars.".to_string(),
return_value: "None.".to_string(),
errors: vec![],
see_also: vec![],
notes: vec![],
bugs: vec![],
examples: vec![],
enable_formatting: false,
arch: "x86_64".to_string(),
};
let plain = man.generate_man_page_plain(&page);
assert!(plain.contains("bar"));
assert!(plain.contains("NAME"));
}
#[test]
fn test_man_generate_for_functions() {
let man = X86DocMan::new("lib", PathBuf::from("d"));
let symbols = vec![X86DocSymbolEntry {
qualified_name: "test_fn".to_string(),
name: "test_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Test function".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: Some("int test_fn(void)".to_string()),
return_type: Some("int".to_string()),
parameters: vec![],
deprecated: false,
since: None,
}];
let mut comments = HashMap::new();
comments.insert(
"test_fn".to_string(),
X86DocComment::from_line_doc("/// \\return 42"),
);
let pages = man.generate_for_functions(&symbols, &comments);
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].0, "test_fn.3");
}
#[test]
fn test_man_all_sections_present() {
let man = X86DocMan::new("pkg", PathBuf::from("d"));
let page = X86ManPage {
name: "full_fn".to_string(),
brief: "A complete function".to_string(),
synopsis: "int full_fn(int a);".to_string(),
description: "Full description.".to_string(),
return_value: "0 on success.".to_string(),
errors: vec![("EACCES".to_string(), "Permission denied".to_string())],
see_also: vec!["other(3)".to_string(), "also(3)".to_string()],
notes: vec!["Important note.".to_string()],
bugs: vec!["Known bug.".to_string()],
examples: vec!["int r = full_fn(1);".to_string()],
enable_formatting: true,
arch: "x86_64".to_string(),
};
let text = man.generate_man_page(&page);
assert!(text.contains("NAME"));
assert!(text.contains("SYNOPSIS"));
assert!(text.contains("DESCRIPTION"));
assert!(text.contains("RETURN VALUE"));
assert!(text.contains("ERRORS"));
assert!(text.contains("NOTES"));
assert!(text.contains("BUGS"));
assert!(text.contains("EXAMPLES"));
assert!(text.contains("SEE ALSO"));
assert!(text.contains("ARCHITECTURE"));
}
#[test]
fn test_xml_new() {
let xml = X86DocXML::new("P", PathBuf::from("xml"));
assert_eq!(xml.project_name, "P");
assert!(xml.pretty_print);
}
#[test]
fn test_xml_validate() {
let xml = X86DocXML::new("", PathBuf::from("x"));
assert!(xml.validate().is_err());
let xml2 = X86DocXML::new("P", PathBuf::from("x"));
assert!(xml2.validate().is_ok());
}
#[test]
fn test_xml_add_compound() {
let mut xml = X86DocXML::new("P", PathBuf::from("x"));
xml.add_compound("class_foo", "class", "Foo", "<dummy/>");
assert_eq!(xml.compounds.len(), 1);
assert_eq!(xml.compound_defs.len(), 1);
}
#[test]
fn test_xml_generate_index_xml() {
let mut xml = X86DocXML::new("P", PathBuf::from("x"));
xml.add_compound("class_foo", "class", "Foo", "");
let index = xml.generate_index_xml();
assert!(index.contains("doxygenindex"));
assert!(index.contains("Foo"));
assert!(index.contains("class_foo"));
}
#[test]
fn test_xml_generate_class_xml() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let members = vec![];
let methods = vec![];
let bases: Vec<String> = vec![];
let result = xml.generate_class_xml(
"class_myclass",
"MyClass",
"class",
Some("Brief"),
Some("Details"),
&members,
&methods,
&bases,
);
assert!(result.contains("compounddef"));
assert!(result.contains("MyClass"));
assert!(result.contains("Brief"));
}
#[test]
fn test_xml_generate_class_with_bases() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let bases = vec!["BaseClass".to_string()];
let result =
xml.generate_class_xml("class_d", "Derived", "class", None, None, &[], &[], &bases);
assert!(result.contains("basecompoundref"));
assert!(result.contains("BaseClass"));
}
#[test]
fn test_xml_generate_function_xml() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let params = vec![(
"x".to_string(),
"int".to_string(),
Some("x param".to_string()),
)];
let result = xml.generate_function_xml(
"func_f",
"f",
"int",
¶ms,
Some("Brief desc"),
Some("Details"),
);
assert!(result.contains("<type>int</type>"));
assert!(result.contains("x"));
assert!(result.contains("Brief desc"));
}
#[test]
fn test_xml_generate_namespace_xml() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let contents = vec![("class".to_string(), "MyClass".to_string())];
let result = xml.generate_namespace_xml("ns_foo", "foo", Some("A namespace"), &contents);
assert!(result.contains("namespace"));
assert!(result.contains("foo"));
assert!(result.contains("MyClass"));
}
#[test]
fn test_xml_generate_output() {
let mut xml = X86DocXML::new("P", PathBuf::from("x"));
xml.add_compound(
"class_a",
"class",
"A",
xml.generate_class_xml("class_a", "A", "class", None, None, &[], &[], &[]),
);
let output = xml.generate();
assert!(output.len() >= 2); }
#[test]
fn test_xml_xml_escape() {
let escaped = X86DocXML::xml_escape("<tag attr=\"val\" & '>");
assert!(!escaped.contains('<'));
assert!(escaped.contains("<"));
assert!(escaped.contains("&"));
}
#[test]
fn test_coverage_new() {
let cov = X86DocCoverage::new();
assert!(cov.enabled);
assert!(cov.warn_undocumented);
assert_eq!(cov.total_symbols, 0);
}
#[test]
fn test_coverage_validate() {
let cov = X86DocCoverage::new();
assert!(cov.validate().is_ok());
}
#[test]
fn test_coverage_validate_bad_pct() {
let mut cov = X86DocCoverage::new();
cov.min_coverage_pct = 150.0;
assert!(cov.validate().is_err());
}
#[test]
fn test_coverage_record_documented_symbol() {
let mut cov = X86DocCoverage::new();
let comment = X86DocComment::from_line_doc("/// Does something.");
cov.record_symbol(
"my_func",
X86DocSymbolKind::Function,
Some("file.c"),
Some("ns"),
None,
Some(&comment),
&[],
);
assert_eq!(cov.total_symbols, 1);
assert_eq!(cov.documented_symbols, 1);
assert_eq!(cov.overall_coverage_pct(), 100.0);
}
#[test]
fn test_coverage_record_undocumented_symbol() {
let mut cov = X86DocCoverage::new();
cov.record_symbol(
"undoc_fn",
X86DocSymbolKind::Function,
Some("file.c"),
None,
None,
None,
&[],
);
assert_eq!(cov.total_symbols, 1);
assert_eq!(cov.documented_symbols, 0);
assert_eq!(cov.overall_coverage_pct(), 0.0);
}
#[test]
fn test_coverage_missing_param_doc() {
let mut cov = X86DocCoverage::new();
let comment = X86DocComment::from_line_doc("/// Brief.");
cov.record_symbol(
"fn",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&comment),
&["x".to_string()],
);
let warnings = cov.get_warnings();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::MissingParamDoc(_))));
}
#[test]
fn test_coverage_missing_return_doc() {
let mut cov = X86DocCoverage::new();
let comment = X86DocComment::from_line_doc("/// Brief.");
cov.record_symbol(
"fn",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&comment),
&[],
);
let warnings = cov.get_warnings();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::MissingReturnDoc)));
}
#[test]
fn test_coverage_per_file_stats() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// Doc'd");
cov.record_symbol(
"f1",
X86DocSymbolKind::Function,
Some("a.c"),
None,
None,
Some(&c),
&[],
);
cov.record_symbol(
"f2",
X86DocSymbolKind::Function,
Some("b.c"),
None,
None,
None,
&[],
);
let stats = cov.file_coverage_stats();
assert_eq!(stats.len(), 2);
}
#[test]
fn test_coverage_per_namespace_stats() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// Doc");
cov.record_symbol(
"f",
X86DocSymbolKind::Function,
Some("f.c"),
Some("ns1"),
None,
Some(&c),
&[],
);
cov.record_symbol(
"g",
X86DocSymbolKind::Function,
Some("f.c"),
Some("ns2"),
None,
None,
&[],
);
let stats = cov.namespace_coverage_stats();
assert_eq!(stats.len(), 2);
}
#[test]
fn test_coverage_per_class_stats() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// Doc");
cov.record_symbol(
"bar",
X86DocSymbolKind::Method,
Some("f.c"),
None,
Some("Foo"),
Some(&c),
&[],
);
let stats = cov.class_coverage_stats();
assert_eq!(stats.len(), 1);
}
#[test]
fn test_coverage_generate_report() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// A function");
cov.record_symbol(
"func",
X86DocSymbolKind::Function,
Some("src/main.c"),
Some("ns"),
None,
Some(&c),
&["param".to_string()],
);
let report = cov.generate_report();
assert!(report.contains("Coverage"));
assert!(report.contains("func"));
}
#[test]
fn test_coverage_reset() {
let mut cov = X86DocCoverage::new();
cov.record_symbol(
"f",
X86DocSymbolKind::Function,
Some("a.c"),
None,
None,
None,
&[],
);
cov.reset();
assert_eq!(cov.total_symbols, 0);
assert_eq!(cov.documented_symbols, 0);
}
#[test]
fn test_coverage_warning_display() {
assert_eq!(
X86DocWarning::Undocumented.to_string(),
"symbol is not documented"
);
assert_eq!(
X86DocWarning::MissingBrief.to_string(),
"missing brief description"
);
assert_eq!(
X86DocWarning::DeprecatedSymbol.to_string(),
"symbol is deprecated"
);
}
#[test]
fn test_coverage_deprecated_warning() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// \\deprecated Use new() instead.");
cov.record_symbol(
"old",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&[],
);
let warnings = cov.get_warnings();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::DeprecatedSymbol)));
}
#[test]
fn test_coverage_calc_pct_zero_denom() {
assert_eq!(X86DocCoverage::calc_pct(0, 0), 100.0);
}
#[test]
fn test_coverage_warning_counts_by_file() {
let mut cov = X86DocCoverage::new();
cov.record_symbol(
"f1",
X86DocSymbolKind::Function,
Some("a.c"),
None,
None,
None,
&[],
);
cov.record_symbol(
"f2",
X86DocSymbolKind::Function,
Some("a.c"),
None,
None,
None,
&[],
);
let counts = cov.warning_count_by_file();
assert_eq!(counts.get("a.c").copied().unwrap_or(0), 2);
}
#[test]
fn test_generator_new() {
let r#gen = X86DocGenerator::new("TestProject", PathBuf::from("doc"));
assert_eq!(r#gen.html.project_name, "TestProject");
assert_eq!(r#gen.arch, X86_DOC_TARGET);
}
#[test]
fn test_generator_new_x86_32() {
let r#gen = X86DocGenerator::new_x86_32("P32", PathBuf::from("doc"));
assert!(r#gen.arch.contains("i386"));
}
#[test]
fn test_generator_default() {
let r#gen = X86DocGenerator::default();
assert!(r#gen.enable_html);
assert!(r#gen.enable_coverage);
}
#[test]
fn test_generator_validate_all_good() {
let r#gen = X86DocGenerator::new("Good", PathBuf::from("doc"));
let result = r#gen.validate();
assert!(result.is_ok());
}
#[test]
fn test_generator_validate_with_errors() {
let mut r#gen = X86DocGenerator::new("Good", PathBuf::from("doc"));
r#gen.html.project_name.clear();
let result = r#gen.validate();
assert!(result.is_err() || result.unwrap().len() > 0);
}
#[test]
fn test_generator_add_class() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
let members: Vec<X86DocSymbolEntry> = vec![];
let methods: Vec<X86DocSymbolEntry> = vec![];
let bases: Vec<String> = vec!["Base".to_string()];
let comment = X86DocComment::from_line_doc("/// Test class");
r#gen.add_class(
"MyClass",
"ns::MyClass",
X86DocSymbolKind::Class,
Some("A test"),
&members,
&methods,
&bases,
Some("ns"),
Some(&comment),
);
}
#[test]
fn test_generator_add_function() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
let sym = X86DocSymbolEntry {
qualified_name: "my_func".to_string(),
name: "my_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Does things".to_string()),
file: Some("file.c".to_string()),
line: Some(5),
parent: None,
link: "func_my_func.html".to_string(),
access: None,
template_params: vec![],
signature: Some("int my_func(void)".to_string()),
return_type: Some("int".to_string()),
parameters: vec![],
deprecated: false,
since: None,
};
let comment = X86DocComment::from_line_doc("/// \\return 0");
r#gen.add_function(&sym, Some(&comment));
}
#[test]
fn test_generator_generate_all() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
r#gen.validate_output = false;
let symbols = vec![X86DocSymbolEntry {
qualified_name: "my_func".to_string(),
name: "my_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("A function".to_string()),
file: None,
line: None,
parent: None,
link: "f.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let mut comments = HashMap::new();
comments.insert(
"my_func".to_string(),
X86DocComment::from_line_doc("/// Brief."),
);
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
let output = result.unwrap();
assert!(!output.html_files.is_empty());
assert!(output.markdown_content.is_some());
assert!(!output.xml_files.is_empty());
assert!(output.coverage_report.is_some());
}
#[test]
fn test_generator_generate_index() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
let index = r#gen.generate_index();
assert!(index.contains("P Documentation"));
assert!(index.contains("Formats"));
}
#[test]
fn test_generator_output_default() {
let output = X86DocOutput::default();
assert!(output.html_files.is_empty());
assert!(output.markdown_content.is_none());
assert!(output.man_pages.is_empty());
}
#[test]
fn test_full_documentation_workflow() {
let mut r#gen = X86DocGenerator::new("TestLib", PathBuf::from("doc"));
r#gen.validate_output = false;
let symbols = vec![
X86DocSymbolEntry {
qualified_name: "init".to_string(),
name: "init".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Initialize the library".to_string()),
file: Some("testlib.h".to_string()),
line: Some(10),
parent: None,
link: "func_init.html".to_string(),
access: None,
template_params: vec![],
signature: Some("int init(int flags)".to_string()),
return_type: Some("int".to_string()),
parameters: vec![("flags".to_string(), "int".to_string())],
deprecated: false,
since: Some("v1.0.0".to_string()),
},
X86DocSymbolEntry {
qualified_name: "cleanup".to_string(),
name: "cleanup".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Clean up the library".to_string()),
file: Some("testlib.h".to_string()),
line: Some(20),
parent: None,
link: "func_cleanup.html".to_string(),
access: None,
template_params: vec![],
signature: Some("void cleanup(void)".to_string()),
return_type: Some("void".to_string()),
parameters: vec![],
deprecated: false,
since: None,
},
];
let mut comments = HashMap::new();
comments.insert(
"init".to_string(),
X86DocComment::from_line_doc(
"/// \\brief Initialize the library.\n/// \\param flags Configuration flags.\n/// \\return 0 on success, -1 on error.\n/// \\note Must be called before any other function.\n/// \\see cleanup",
),
);
comments.insert(
"cleanup".to_string(),
X86DocComment::from_line_doc(
"/// \\brief Clean up the library.\n/// \\note Call after all operations are complete.",
),
);
for sym in &symbols {
let c = comments.get(&sym.qualified_name as &str);
r#gen.add_function(sym, c);
}
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.html_files.len() > 0);
let html_count = output
.html_files
.iter()
.filter(|(p, _)| p.to_string_lossy().ends_with(".html"))
.count();
assert!(html_count > 0);
let md = output.markdown_content.unwrap();
assert!(md.contains("init"));
assert!(md.contains("cleanup"));
assert!(!output.man_pages.is_empty());
assert!(output.man_pages.iter().any(|(n, _)| n.contains("init")));
assert!(!output.xml_files.is_empty());
assert!(output
.xml_files
.iter()
.any(|(p, _)| p.to_string_lossy() == "index.xml"));
let coverage = output.coverage_report.unwrap();
assert!(coverage.contains("100.0%"));
}
#[test]
fn test_documentation_with_full_commands() {
let raw = r#"/// \brief Compute the factorial.
/// \details Uses an iterative approach to compute n! for n >= 0.
///
/// \param n The integer to compute the factorial of.
/// \return The factorial of n.
/// \retval 0 factorial of 0 is 1
/// \throws std::invalid_argument If n is negative.
/// \note This function is not safe for large n (n > 20).
/// \warning Integer overflow may occur for n > 12 on 32-bit systems.
/// \see factorial_ll
/// \deprecated Use factorial_v2 instead.
/// \since v1.0.0
/// \tparam IntType The integer type for computation.
/// \code
/// int r = factorial(5); // r = 120
/// \endcode"#;
let comment = X86DocComment::from_line_doc(raw);
assert!(!comment.is_empty);
assert_eq!(comment.brief.unwrap(), "Compute the factorial.");
assert!(comment.details.is_some());
assert_eq!(comment.params.len(), 1);
assert_eq!(comment.params[0].0, "n");
assert!(comment.return_desc.is_some());
assert_eq!(comment.retvals.len(), 1);
assert_eq!(comment.throws.len(), 1);
assert_eq!(comment.notes.len(), 1);
assert_eq!(comment.warnings.len(), 1);
assert_eq!(comment.see_also.len(), 1);
assert!(comment.deprecated);
assert_eq!(comment.since.unwrap(), "v1.0.0");
assert_eq!(comment.tparams.len(), 1);
assert_eq!(comment.code_blocks.len(), 1);
}
#[test]
fn test_markdown_rendering_preserves_structure() {
let raw = "/// # Main Title\n/// ## Subtitle\n/// Some paragraph.\n/// - List item 1\n/// - List item 2\n///\n/// ```\n/// code block\n/// ```";
let comment = X86DocComment::from_line_doc(raw);
let html = comment.to_html();
assert!(html.contains("<h1>"));
assert!(html.contains("<h2>"));
assert!(html.contains("<ul>"));
assert!(html.contains("<pre><code>"));
}
#[test]
fn test_html_page_generation_has_all_elements() {
let mut html_gen = X86DocHTML::new("TestLib", PathBuf::from("doc"));
let members: Vec<X86DocSymbolEntry> = vec![X86DocSymbolEntry {
qualified_name: "m_value".to_string(),
name: "m_value".to_string(),
kind: X86DocSymbolKind::Field,
brief: Some("The stored value".to_string()),
file: None,
line: None,
parent: None,
link: "class_TestClass.html#m_value".to_string(),
access: Some(X86DocAccess::Private),
template_params: vec![],
signature: None,
return_type: Some("int".to_string()),
parameters: vec![],
deprecated: false,
since: None,
}];
let methods: Vec<X86DocSymbolEntry> = vec![X86DocSymbolEntry {
qualified_name: "getValue".to_string(),
name: "getValue".to_string(),
kind: X86DocSymbolKind::Method,
brief: Some("Get the value".to_string()),
file: None,
line: None,
parent: None,
link: "class_TestClass.html#getValue".to_string(),
access: Some(X86DocAccess::Public),
template_params: vec![],
signature: Some("int getValue() const".to_string()),
return_type: Some("int".to_string()),
parameters: vec![],
deprecated: false,
since: None,
}];
let bases = vec!["BaseClass".to_string()];
let result = html_gen.render_class_page(
"TestClass",
"TestClass",
X86DocSymbolKind::Class,
Some("A test class with members"),
&members,
&methods,
&bases,
None,
);
assert!(result.contains("TestClass"));
assert!(result.contains("m_value"));
assert!(result.contains("getValue"));
assert!(result.contains("BaseClass"));
assert!(result.contains("private"));
assert!(result.contains("public"));
}
#[test]
fn test_xml_roundtrip_class() {
let mut xml_gen = X86DocXML::new("TestLib", PathBuf::from("doc/xml"));
let class_xml = xml_gen.generate_class_xml(
"class_RT",
"RT",
"struct",
Some("Round-trip test struct"),
Some("Details for testing."),
&[],
&[],
&[],
);
xml_gen.add_compound("class_RT", "struct", "RT", &class_xml);
let index = xml_gen.generate_index_xml();
assert!(index.contains("class_RT"));
assert!(index.contains("RT"));
let output = xml_gen.generate();
assert!(output
.iter()
.any(|(p, _)| p.to_string_lossy() == "class_RT.xml"));
}
#[test]
fn test_coverage_tracks_all_symbol_kinds() {
let mut cov = X86DocCoverage::new();
let kinds = vec![
X86DocSymbolKind::Function,
X86DocSymbolKind::Method,
X86DocSymbolKind::Class,
X86DocSymbolKind::Variable,
X86DocSymbolKind::Enum,
];
for (i, kind) in kinds.iter().enumerate() {
cov.record_symbol(
&format!("sym_{}", i),
*kind,
Some("all.c"),
None,
None,
None,
&[],
);
}
assert_eq!(cov.total_symbols, 5);
assert_eq!(cov.documented_symbols, 0);
}
#[test]
fn test_generator_formats_disabled() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
r#gen.enable_html = false;
r#gen.enable_markdown = false;
r#gen.enable_man = false;
r#gen.enable_xml = false;
r#gen.validate_output = false;
let symbols = vec![];
let comments = HashMap::new();
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.html_files.is_empty());
assert!(output.markdown_content.is_some()); assert!(output.man_pages.is_empty());
assert!(output.xml_files.is_empty());
}
#[test]
fn test_unknown_param_warning() {
let mut cov = X86DocCoverage::new();
let raw = "/// \\param x The x value.\n/// \\param y The y value.";
let c = X86DocComment::from_line_doc(raw);
cov.record_symbol(
"fn",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&["x".to_string()], );
let warnings = cov.get_warnings();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::UnknownParam(_, _))));
}
#[test]
fn test_documentation_coverage_50_percent() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// Doc");
cov.record_symbol(
"a",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&[],
);
cov.record_symbol(
"b",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
None,
&[],
);
assert_eq!(cov.overall_coverage_pct(), 50.0);
}
#[test]
fn test_html_escape_handles_special_chars() {
let escaped = X86DocHTML::html_escape("<script>alert('xss')</script>");
assert!(!escaped.contains('<'));
assert!(!escaped.contains('>'));
assert!(escaped.contains("<"));
assert!(escaped.contains(">"));
}
#[test]
fn test_markdown_escape_code() {
let escaped = X86DocMarkdown::escape_code("foo|bar`baz");
assert_eq!(escaped, "foo\\|bar\\`baz");
}
#[test]
fn test_man_escape_is_safe() {
let escaped = X86DocMan::escape_man("hello-world's test");
assert!(escaped.contains("\\-"));
}
#[test]
fn test_default_constants_exist() {
assert!(!X86_DOC_DEFAULT_CSS.is_empty());
assert!(!X86_DOC_DEFAULT_JS.is_empty());
assert!(X86_DOC_MAX_BRIEF_LEN > 0);
assert!(X86_DOC_MAX_RECURSION > 0);
}
#[test]
fn test_comment_validdate_has_all_warnings() {
let c = X86DocComment::new();
let w = c.validate();
assert!(!w.is_empty());
}
#[test]
fn test_html_js_string_escape() {
let s = X86DocHTML::js_string("hello \"world\"\nnew");
assert!(s.contains("\\\""));
assert!(s.contains("\\n"));
}
#[test]
fn test_markdown_anchor_generation() {
let anchor = X86DocMarkdown::make_anchor("Hello World! @#$%");
assert_eq!(anchor, "hello-world");
}
#[test]
fn test_markdown_capitalize() {
assert_eq!(X86DocMarkdown::capitalize("hello"), "Hello");
assert_eq!(X86DocMarkdown::capitalize("Hello"), "Hello");
}
#[test]
fn test_man_center_text() {
let centered = X86DocMan::center_text("HI", 10);
assert_eq!(centered.len(), 10);
assert!(centered.contains("HI"));
}
#[test]
fn test_coverage_file_stats_sorted() {
let mut cov = X86DocCoverage::new();
cov.record_symbol(
"z",
X86DocSymbolKind::Function,
Some("z.c"),
None,
None,
None,
&[],
);
cov.record_symbol(
"a",
X86DocSymbolKind::Function,
Some("a.c"),
None,
None,
None,
&[],
);
let stats = cov.file_coverage_stats();
assert_eq!(stats[0].file_path, "a.c");
assert_eq!(stats[1].file_path, "z.c");
}
#[test]
fn test_xml_pretty_print_disabled() {
let mut xml = X86DocXML::new("P", PathBuf::from("x"));
xml.pretty_print = false;
xml.add_compound("c_a", "class", "A", "<d/>");
let idx = xml.generate_index_xml();
assert!(!idx.contains("\n"));
}
#[test]
fn test_generator_with_empty_symbols() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
r#gen.validate_output = false;
let symbols = vec![];
let comments = HashMap::new();
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
}
#[test]
fn test_html_page_kind_all_variants() {
let kinds = vec![
X86DocPageKind::Index,
X86DocPageKind::Namespace,
X86DocPageKind::Class,
X86DocPageKind::Function,
X86DocPageKind::Enum,
X86DocPageKind::TypeDef,
X86DocPageKind::File,
X86DocPageKind::Module,
X86DocPageKind::HierarchicalIndex,
X86DocPageKind::Search,
X86DocPageKind::Topic,
];
for kind in kinds {
let s = kind.to_string();
assert!(!s.is_empty());
}
}
#[test]
fn test_html_symbol_kind_all_variants() {
let kinds = vec![
X86DocSymbolKind::Namespace,
X86DocSymbolKind::Class,
X86DocSymbolKind::Struct,
X86DocSymbolKind::Union,
X86DocSymbolKind::Enum,
X86DocSymbolKind::EnumValue,
X86DocSymbolKind::Function,
X86DocSymbolKind::Method,
X86DocSymbolKind::Constructor,
X86DocSymbolKind::Destructor,
X86DocSymbolKind::Operator,
X86DocSymbolKind::Variable,
X86DocSymbolKind::Field,
X86DocSymbolKind::TypeDef,
X86DocSymbolKind::Using,
X86DocSymbolKind::Macro,
X86DocSymbolKind::Module,
X86DocSymbolKind::Concept,
X86DocSymbolKind::Template,
X86DocSymbolKind::Alias,
];
for kind in kinds {
let s = kind.to_string();
assert!(!s.is_empty());
}
}
#[test]
fn test_html_access_all_variants() {
assert_eq!(X86DocAccess::Public.to_string(), "public");
assert_eq!(X86DocAccess::Protected.to_string(), "protected");
assert_eq!(X86DocAccess::Private.to_string(), "private");
}
#[test]
fn test_comment_parse_author() {
let raw = "/// \\author John Smith";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_date() {
let raw = "/// \\date 2025-06-15";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_version() {
let raw = "/// \\version 2.1.0";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_todo() {
let raw = "/// \\todo Implement error handling.";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_bug() {
let raw = "/// \\bug Memory leak on error path.";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_test() {
let raw = "/// \\test test_foo_basic";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
}
#[test]
fn test_comment_parse_brief_at_sign() {
let raw = "/// @brief Short description.";
let c = X86DocComment::from_line_doc(raw);
assert!(c.brief.is_some());
assert_eq!(c.brief.unwrap(), "Short description.");
}
#[test]
fn test_comment_parse_empty_param() {
let raw = "/// \\param x";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.params.len(), 1);
assert_eq!(c.params[0].0, "x");
assert!(c.params[0].1.is_empty());
}
#[test]
fn test_comment_parse_empty_retval() {
let raw = "/// \\retval 0";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.retvals.len(), 1);
assert_eq!(c.retvals[0].0, "0");
assert!(c.retvals[0].1.is_empty());
}
#[test]
fn test_comment_parse_code_block_multiline() {
let raw = "/// \\code\n/// line1\n/// line2\n/// \\endcode";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.code_blocks.len(), 1);
}
#[test]
fn test_comment_parse_unterminated_code_block() {
let raw = "/// \\code\n/// line1\n/// line2";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.code_blocks.len(), 1);
}
#[test]
fn test_comment_parse_unterminated_verbatim() {
let raw = "/// \\verbatim\n/// raw text";
let c = X86DocComment::from_line_doc(raw);
assert_eq!(c.verbatim_blocks.len(), 1);
}
#[test]
fn test_comment_to_html_empty() {
let c = X86DocComment::new();
let html = c.to_html();
assert!(html.is_empty());
}
#[test]
fn test_comment_to_html_bold_italic_code() {
let raw = "/// **bold** *italic* `code`";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<strong>"));
assert!(html.contains("<em>"));
assert!(html.contains("<code>"));
}
#[test]
fn test_comment_to_html_blockquote() {
let raw = "/// > This is a quote";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<blockquote>"));
}
#[test]
fn test_comment_to_html_horizontal_rule() {
for marker in &["---", "***", "___"] {
let raw = &format!("/// {}", marker);
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<hr>"), "Failed for marker: {}", marker);
}
}
#[test]
fn test_comment_to_html_table() {
let raw = "/// | A | B |\n/// |---|---|\n/// | 1 | 2 |";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<table>"));
assert!(html.contains("<th>A</th>"));
assert!(html.contains("<td>1</td>"));
}
#[test]
fn test_comment_to_html_ordered_list() {
let raw = "/// 1. First\n/// 2. Second\n/// 3. Third";
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains("<ol>"));
assert!(html.contains("<li>"));
}
#[test]
fn test_comment_to_html_heading_levels() {
for i in 1..=6 {
let prefix = "#".repeat(i);
let raw = &format!("/// {} Heading", prefix);
let c = X86DocComment::from_line_doc(raw);
let html = c.to_html();
assert!(html.contains(&format!("<h{}>", i)));
}
}
#[test]
fn test_comment_empty_then_non_empty() {
let c1 = X86DocComment::new();
assert!(c1.is_empty);
let c2 = X86DocComment::from_line_doc("/// content");
assert!(!c2.is_empty);
}
#[test]
fn test_html_render_symbol_list_empty() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let result = html.render_symbol_list(&[]);
assert!(result.contains("No symbols"));
}
#[test]
fn test_html_render_symbol_list_with_items() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let symbols = vec![X86DocSymbolEntry {
qualified_name: "MyFunc".to_string(),
name: "MyFunc".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Does work".to_string()),
file: Some("file.h".to_string()),
line: Some(10),
parent: None,
link: "func_myfunc.html".to_string(),
access: None,
template_params: vec![],
signature: Some("void MyFunc()".to_string()),
return_type: None,
parameters: vec![("x".to_string(), "int".to_string())],
deprecated: true,
since: Some("v1.0".to_string()),
}];
let result = html.render_symbol_list(&symbols);
assert!(result.contains("MyFunc"));
assert!(result.contains("deprecated"));
assert!(result.contains("v1.0"));
assert!(result.contains("file.h:10"));
}
#[test]
fn test_html_render_breadcrumb_empty() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let result = html.render_breadcrumb(&[]);
assert!(result.is_empty());
}
#[test]
fn test_html_render_breadcrumb_with_items() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let crumbs = vec![
("Home".to_string(), "index.html".to_string()),
("Namespace".to_string(), "ns.html".to_string()),
("Class".to_string(), "".to_string()),
];
let result = html.render_breadcrumb(&crumbs);
assert!(result.contains("Home"));
assert!(result.contains("Namespace"));
assert!(result.contains("Class"));
assert!(result.contains("index.html"));
}
#[test]
fn test_html_render_nav_node() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let node = X86DocNavNode {
name: "Test".to_string(),
link: Some("test.html".to_string()),
kind: X86DocSymbolKind::Class,
children: vec![],
collapsed: false,
};
let result = html.render_nav_node(&node, 0);
assert!(result.contains("Test"));
}
#[test]
fn test_html_render_nav_node_with_children() {
let html = X86DocHTML::new("P", PathBuf::from("d"));
let child = X86DocNavNode {
name: "Child".to_string(),
link: Some("c.html".to_string()),
kind: X86DocSymbolKind::Method,
children: vec![],
collapsed: false,
};
let node = X86DocNavNode {
name: "Parent".to_string(),
link: None,
kind: X86DocSymbolKind::Class,
children: vec![child],
collapsed: true,
};
let result = html.render_nav_node(&node, 1);
assert!(result.contains("Parent"));
assert!(result.contains("Child"));
assert!(result.contains("nested"));
}
#[test]
fn test_html_generate_with_search_disabled() {
let mut html = X86DocHTML::new("P", PathBuf::from("d"));
html.generate_search = false;
let page = X86DocPage {
title: "T".to_string(),
kind: X86DocPageKind::Class,
output_file: PathBuf::from("t.html"),
breadcrumb: vec![],
body_html: "<p>t</p>".to_string(),
symbols: vec![],
sub_pages: vec![],
};
html.add_page(page);
let result = html.generate();
assert!(result.is_ok());
}
#[test]
fn test_markdown_generate_empty() {
let md = X86DocMarkdown::new("P", PathBuf::from("f"));
let output = md.generate();
assert!(output.contains("P Documentation"));
}
#[test]
fn test_markdown_with_all_features() {
let mut md = X86DocMarkdown::new("FullTest", PathBuf::from("f"));
md.generate_toc = true;
md.toc_max_depth = 4;
md.include_api_reference = true;
md.gfm = true;
md.add_section(2, "Overview", "Project overview text.");
md.add_section(2, "Installation", "Install with cmake.");
md.add_section(3, "Dependencies", "Requires C++17.");
md.add_section(3, "Build", "Standard cmake build.");
md.add_section(2, "API Reference", "");
md.add_section(4, "init()", "Initializes.");
let output = md.generate();
assert!(output.contains("Overview"));
assert!(output.contains("Installation"));
assert!(output.contains("Dependencies"));
assert!(output.contains("Build"));
assert!(output.contains("Table of Contents"));
}
#[test]
fn test_markdown_toc_respects_max_depth() {
let mut md = X86DocMarkdown::new("P", PathBuf::from("f"));
md.toc_max_depth = 2;
md.add_section(2, "A", "");
md.add_section(3, "B", ""); md.add_section(4, "C", ""); assert_eq!(md.toc_entries.len(), 2);
}
#[test]
fn test_man_generate_multiple_pages() {
let man = X86DocMan::new("pkg", PathBuf::from("d"));
let symbols = vec![
X86DocSymbolEntry {
qualified_name: "fn1".to_string(),
name: "fn1".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("First".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
},
X86DocSymbolEntry {
qualified_name: "fn2".to_string(),
name: "fn2".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Second".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
},
];
let comments = HashMap::new();
let pages = man.generate_for_functions(&symbols, &comments);
assert_eq!(pages.len(), 2);
}
#[test]
fn test_man_skips_non_function_symbols() {
let man = X86DocMan::new("pkg", PathBuf::from("d"));
let symbols = vec![
X86DocSymbolEntry {
qualified_name: "MyClass".to_string(),
name: "MyClass".to_string(),
kind: X86DocSymbolKind::Class,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
},
X86DocSymbolEntry {
qualified_name: "my_func".to_string(),
name: "my_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("A function".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
},
];
let comments = HashMap::new();
let pages = man.generate_for_functions(&symbols, &comments);
assert_eq!(pages.len(), 1);
}
#[test]
fn test_xml_add_same_compound_twice() {
let mut xml = X86DocXML::new("P", PathBuf::from("x"));
xml.add_compound("id", "class", "Name", "<a/>");
xml.add_compound("id", "class", "Name", "<b/>");
assert_eq!(xml.compounds.len(), 2);
}
#[test]
fn test_xml_generate_class_with_members_and_methods() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let members = vec![(
"public-attrib".to_string(),
"m_x".to_string(),
Some("The x value".to_string()),
)];
let methods = vec![(
"public-func".to_string(),
"getX".to_string(),
Some("Get x".to_string()),
)];
let bases: Vec<String> = vec![];
let result =
xml.generate_class_xml("c", "C", "class", None, None, &members, &methods, &bases);
assert!(result.contains("m_x"));
assert!(result.contains("getX"));
assert!(result.contains("memberdef"));
}
#[test]
fn test_xml_generate_function_with_all_params() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let params = vec![
(
"a".to_string(),
"int".to_string(),
Some("First param".to_string()),
),
("b".to_string(), "float".to_string(), None),
];
let result = xml.generate_function_xml("f", "f", "void", ¶ms, None, None);
assert!(result.contains("a"));
assert!(result.contains("b"));
assert!(result.contains("First param"));
}
#[test]
fn test_coverage_all_warning_types_have_output() {
let warnings = vec![
X86DocWarning::Undocumented,
X86DocWarning::MissingBrief,
X86DocWarning::MissingParamDoc("x".to_string()),
X86DocWarning::MissingReturnDoc,
X86DocWarning::MissingTParamDoc("T".to_string()),
X86DocWarning::DeprecatedSymbol,
X86DocWarning::UnknownParam("f".to_string(), "y".to_string()),
X86DocWarning::LowCoverage("ns".to_string(), 45.0),
];
for w in &warnings {
let s = w.to_string();
assert!(!s.is_empty(), "Empty string for {:?}", w);
}
}
#[test]
fn test_coverage_multiple_files() {
let mut cov = X86DocCoverage::new();
for i in 0..10 {
let filename = format!("file_{}.c", i);
cov.record_symbol(
&format!("fn_{}", i),
X86DocSymbolKind::Function,
Some(&filename),
None,
None,
None,
&[],
);
}
let stats = cov.file_coverage_stats();
assert_eq!(stats.len(), 10);
}
#[test]
fn test_coverage_namespace_nesting() {
let mut cov = X86DocCoverage::new();
let ns_list = vec!["outer", "outer::inner", "outer::inner::deep"];
for ns in &ns_list {
cov.record_symbol(
"fn",
X86DocSymbolKind::Function,
Some("f.c"),
Some(ns),
None,
None,
&[],
);
}
assert_eq!(cov.namespace_coverage_stats().len(), 3);
}
#[test]
fn test_generator_has_all_components() {
let r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
assert!(!r#gen.html.project_name.is_empty());
assert!(!r#gen.markdown.project_name.is_empty());
assert!(!r#gen.man.package.is_empty());
assert!(!r#gen.xml.project_name.is_empty());
}
#[test]
fn test_doc_output_has_all_fields() {
let output = X86DocOutput {
html_files: vec![(PathBuf::from("a.html"), "<html>".to_string())],
markdown_content: Some("# MD".to_string()),
man_pages: vec![("fn.3".to_string(), ".TH FN 3".to_string())],
xml_files: vec![(PathBuf::from("index.xml"), "<doxygen>".to_string())],
coverage_report: Some("100%".to_string()),
validation_messages: vec!["ok".to_string()],
};
assert_eq!(output.html_files.len(), 1);
assert!(output.markdown_content.is_some());
assert_eq!(output.man_pages.len(), 1);
assert_eq!(output.xml_files.len(), 1);
assert!(output.coverage_report.is_some());
assert_eq!(output.validation_messages.len(), 1);
}
#[test]
fn test_comment_extract_first_paragraph_short() {
let text = "Short brief.";
let brief = X86DocComment::extract_first_paragraph(text);
assert_eq!(brief.unwrap(), "Short brief.");
}
#[test]
fn test_comment_extract_first_paragraph_multiline() {
let text = "Line one.\nLine two.\n\nLine after blank.";
let brief = X86DocComment::extract_first_paragraph(text);
assert_eq!(brief.unwrap(), "Line one. Line two.");
}
#[test]
fn test_comment_extract_first_paragraph_long_truncated() {
let text = "A".repeat(300);
let brief = X86DocComment::extract_first_paragraph(&text);
assert!(brief.unwrap().ends_with("..."));
}
#[test]
fn test_comment_strip_block_leading_stars() {
let raw = " * Line one\n * Line two\n Unstarred";
let result = X86DocComment::strip_block_leading_stars(raw);
assert!(result.contains("Line one"));
assert!(result.contains("Line two"));
assert!(result.contains("Unstarred"));
}
#[test]
fn test_symbol_kind_display_all_unique() {
use std::collections::HashSet;
let mut seen = HashSet::new();
let kinds = vec![
X86DocSymbolKind::Namespace,
X86DocSymbolKind::Class,
X86DocSymbolKind::Struct,
X86DocSymbolKind::Union,
X86DocSymbolKind::Enum,
X86DocSymbolKind::EnumValue,
X86DocSymbolKind::Function,
X86DocSymbolKind::Method,
X86DocSymbolKind::Constructor,
X86DocSymbolKind::Destructor,
X86DocSymbolKind::Operator,
X86DocSymbolKind::Variable,
X86DocSymbolKind::Field,
X86DocSymbolKind::TypeDef,
X86DocSymbolKind::Using,
X86DocSymbolKind::Macro,
X86DocSymbolKind::Module,
X86DocSymbolKind::Concept,
X86DocSymbolKind::Template,
X86DocSymbolKind::Alias,
];
for kind in kinds {
assert!(
seen.insert(kind.to_string()),
"Duplicate: {}",
kind.to_string()
);
}
}
#[test]
fn test_full_doc_workflow_with_all_features() {
let mut r#gen = X86DocGenerator::new("FullTest", PathBuf::from("doc"));
r#gen.validate_output = false;
r#gen.enable_html = true;
r#gen.enable_markdown = true;
r#gen.enable_man = true;
r#gen.enable_xml = true;
r#gen.enable_coverage = true;
let symbols: Vec<X86DocSymbolEntry> = vec![
X86DocSymbolEntry {
qualified_name: "create_widget".to_string(),
name: "create_widget".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Create a new widget".to_string()),
file: Some("widget.h".to_string()),
line: Some(42),
parent: None,
link: "func_create_widget.html".to_string(),
access: None,
template_params: vec![],
signature: Some("Widget* create_widget(const char* name, int flags)".to_string()),
return_type: Some("Widget*".to_string()),
parameters: vec![
("name".to_string(), "const char*".to_string()),
("flags".to_string(), "int".to_string()),
],
deprecated: false,
since: Some("v2.1.0".to_string()),
},
X86DocSymbolEntry {
qualified_name: "destroy_widget".to_string(),
name: "destroy_widget".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Destroy a widget".to_string()),
file: Some("widget.h".to_string()),
line: Some(55),
parent: None,
link: "func_destroy_widget.html".to_string(),
access: None,
template_params: vec![],
signature: Some("void destroy_widget(Widget* w)".to_string()),
return_type: Some("void".to_string()),
parameters: vec![("w".to_string(), "Widget*".to_string())],
deprecated: false,
since: None,
},
X86DocSymbolEntry {
qualified_name: "widget_get_name".to_string(),
name: "widget_get_name".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Get widget name".to_string()),
file: Some("widget.h".to_string()),
line: Some(60),
parent: None,
link: "func_widget_get_name.html".to_string(),
access: None,
template_params: vec![],
signature: Some("const char* widget_get_name(const Widget* w)".to_string()),
return_type: Some("const char*".to_string()),
parameters: vec![("w".to_string(), "const Widget*".to_string())],
deprecated: true,
since: None,
},
];
let mut comments = HashMap::new();
comments.insert(
"create_widget".to_string(),
X86DocComment::from_line_doc(
"/// \\brief Create a new widget.\n/// \\param name Widget name (must not be NULL).\n/// \\param flags Creation flags (bitmask).\n/// \\return Newly allocated Widget, or NULL on error.\n/// \\note The returned Widget must be freed with destroy_widget().\n/// \\see destroy_widget\n/// \\since v2.1.0",
),
);
comments.insert(
"destroy_widget".to_string(),
X86DocComment::from_line_doc(
"/// \\brief Destroy a widget.\n/// \\param w Widget to destroy (may be NULL, in which case this is a no-op).",
),
);
comments.insert(
"widget_get_name".to_string(),
X86DocComment::from_line_doc(
"/// \\brief Get widget name.\n/// \\deprecated Use widget_get_display_name() instead.\n/// \\param w Widget to query.\n/// \\return Widget name string.",
),
);
for sym in &symbols {
let c = comments.get(&sym.qualified_name as &str);
r#gen.add_function(sym, c);
}
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
let output = result.unwrap();
assert!(!output.html_files.is_empty());
let md = output.markdown_content.unwrap();
assert!(md.contains("create_widget"));
assert!(md.contains("destroy_widget"));
assert_eq!(output.man_pages.len(), 3);
assert!(!output.xml_files.is_empty());
let coverage = output.coverage_report.unwrap();
assert!(
coverage.contains("DeprecatedSymbol")
|| coverage.contains("deprecated")
|| coverage.len() > 0
);
}
#[test]
fn test_generator_cross_format_consistency() {
let mut r#gen = X86DocGenerator::new("CrossTest", PathBuf::from("doc"));
r#gen.validate_output = false;
let sym = X86DocSymbolEntry {
qualified_name: "cross_fn".to_string(),
name: "cross_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("Cross-format test".to_string()),
file: Some("cross.h".to_string()),
line: Some(1),
parent: None,
link: "f.html".to_string(),
access: None,
template_params: vec![],
signature: Some("int cross_fn(int a, int b)".to_string()),
return_type: Some("int".to_string()),
parameters: vec![
("a".to_string(), "int".to_string()),
("b".to_string(), "int".to_string()),
],
deprecated: false,
since: None,
};
let comment = X86DocComment::from_line_doc(
"/// \\brief Cross-format test.\n/// \\param a First operand.\n/// \\param b Second operand.\n/// \\return Sum.",
);
let mut comments = HashMap::new();
comments.insert("cross_fn".to_string(), comment);
r#gen.add_function(&sym, Some(&comments["cross_fn"]));
let result = r#gen.generate_all(&[sym], &comments);
assert!(result.is_ok());
let output = result.unwrap();
for (_, html) in &output.html_files {
if html.contains("cross_fn") {
break;
}
}
let md = output.markdown_content.unwrap();
assert!(md.contains("cross_fn"));
let man_text = &output.man_pages[0].1;
assert!(man_text.contains("cross_fn"));
let xml_text = &output
.xml_files
.iter()
.find(|(p, _)| !p.to_string_lossy().contains("index"))
.map(|(_, v)| v)
.unwrap();
assert!(xml_text.contains("cross_fn"));
}
#[test]
fn test_json_new() {
let json = X86DocJSON::new("P", PathBuf::from("doc.json"));
assert_eq!(json.project_name, "P");
assert!(json.pretty_print);
}
#[test]
fn test_json_validate_empty() {
let json = X86DocJSON::new("", PathBuf::from("d"));
assert!(json.validate().is_err());
}
#[test]
fn test_json_validate_ok() {
let json = X86DocJSON::new("P", PathBuf::from("d"));
assert!(json.validate().is_ok());
}
#[test]
fn test_json_add_symbol_no_comment() {
let mut json = X86DocJSON::new("P", PathBuf::from("d"));
let sym = X86DocSymbolEntry {
qualified_name: "fn".to_string(),
name: "fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("brief".to_string()),
file: Some("f.c".to_string()),
line: Some(1),
parent: Some("ns".to_string()),
link: "link".to_string(),
access: Some(X86DocAccess::Public),
template_params: vec!["T".to_string()],
signature: Some("int fn()".to_string()),
return_type: Some("int".to_string()),
parameters: vec![("x".to_string(), "int".to_string())],
deprecated: true,
since: Some("v1".to_string()),
};
json.add_symbol(&sym, None);
let output = json.generate();
assert!(output.contains("fn"));
assert!(output.contains("int"));
}
#[test]
fn test_json_add_symbol_with_comment() {
let mut json = X86DocJSON::new("P", PathBuf::from("d"));
let sym = X86DocSymbolEntry {
qualified_name: "fn".to_string(),
name: "fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "l".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
let comment = X86DocComment::from_line_doc("/// \\brief Test.\n/// \\return 42");
json.add_symbol(&sym, Some(&comment));
let output = json.generate();
assert!(output.contains("doc_fn"));
assert!(output.contains("brief"));
}
#[test]
fn test_json_generate_pretty_print() {
let mut json = X86DocJSON::new("P", PathBuf::from("d"));
json.pretty_print = true;
let sym = X86DocSymbolEntry {
qualified_name: "a".to_string(),
name: "a".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
json.add_symbol(&sym, None);
let output = json.generate();
assert!(output.contains("\n"));
}
#[test]
fn test_json_generate_compact() {
let mut json = X86DocJSON::new("P", PathBuf::from("d"));
json.pretty_print = false;
let sym = X86DocSymbolEntry {
qualified_name: "a".to_string(),
name: "a".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
json.add_symbol(&sym, None);
let output = json.generate();
assert!(!output.contains("\n"));
}
#[test]
fn test_json_default() {
let json = X86DocJSON::default();
assert!(!json.project_name.is_empty());
}
#[test]
fn test_json_multiple_symbols() {
let mut json = X86DocJSON::new("P", PathBuf::from("d"));
for i in 0..5 {
let sym = X86DocSymbolEntry {
qualified_name: format!("fn_{}", i),
name: format!("fn_{}", i),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
json.add_symbol(&sym, None);
}
let output = json.generate();
assert!(output.contains("fn_0"));
assert!(output.contains("fn_4"));
}
#[test]
fn test_json_escape_special_chars() {
let s = X86DocJSON::json_escape("hello \"world\"\nnew\ttab");
assert!(s.contains("\\\""));
assert!(s.contains("\\n"));
assert!(s.contains("\\t"));
}
#[test]
fn test_diff_new() {
let diff = X86DocDiff::new();
assert!(diff.baseline.is_empty());
assert!(diff.current.is_empty());
}
#[test]
fn test_diff_validate() {
let diff = X86DocDiff::new();
assert!(diff.validate().is_ok());
}
#[test]
fn test_diff_load_baseline() {
let mut diff = X86DocDiff::new();
let symbols = vec![X86DocSymbolEntry {
qualified_name: "f".to_string(),
name: "f".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let comments = HashMap::new();
diff.load_baseline(&symbols, &comments);
assert_eq!(diff.baseline.len(), 1);
}
#[test]
fn test_diff_no_changes() {
let mut diff = X86DocDiff::new();
let symbols = vec![X86DocSymbolEntry {
qualified_name: "f".to_string(),
name: "f".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let comments = HashMap::new();
diff.load_baseline(&symbols, &comments);
diff.load_current(&symbols, &comments);
let result = diff.compute_diff();
assert_eq!(result.stats.added_count, 0);
assert_eq!(result.stats.removed_count, 0);
assert_eq!(result.stats.unchanged_count, 1);
}
#[test]
fn test_diff_added_symbol() {
let mut diff = X86DocDiff::new();
let old: Vec<X86DocSymbolEntry> = vec![];
let new = vec![X86DocSymbolEntry {
qualified_name: "new_fn".to_string(),
name: "new_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let comments = HashMap::new();
diff.load_baseline(&old, &comments);
diff.load_current(&new, &comments);
let result = diff.compute_diff();
assert_eq!(result.stats.added_count, 1);
}
#[test]
fn test_diff_removed_symbol() {
let mut diff = X86DocDiff::new();
let old = vec![X86DocSymbolEntry {
qualified_name: "old_fn".to_string(),
name: "old_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let new: Vec<X86DocSymbolEntry> = vec![];
let comments = HashMap::new();
diff.load_baseline(&old, &comments);
diff.load_current(&new, &comments);
let result = diff.compute_diff();
assert_eq!(result.stats.removed_count, 1);
}
#[test]
fn test_diff_brief_changed() {
let mut diff = X86DocDiff::new();
let sym = X86DocSymbolEntry {
qualified_name: "fn".to_string(),
name: "fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
let mut old_comments = HashMap::new();
old_comments.insert(
"fn".to_string(),
X86DocComment::from_line_doc("/// Old brief."),
);
let mut new_comments = HashMap::new();
new_comments.insert(
"fn".to_string(),
X86DocComment::from_line_doc("/// New brief."),
);
diff.load_baseline(&[sym.clone()], &old_comments);
diff.load_current(&[sym], &new_comments);
let result = diff.compute_diff();
assert_eq!(result.stats.modified_count, 1);
}
#[test]
fn test_diff_doc_added() {
let mut diff = X86DocDiff::new();
let sym = X86DocSymbolEntry {
qualified_name: "fn".to_string(),
name: "fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
let old_comments: HashMap<String, X86DocComment> = HashMap::new();
let mut new_comments = HashMap::new();
new_comments.insert(
"fn".to_string(),
X86DocComment::from_line_doc("/// Now documented."),
);
diff.load_baseline(&[sym.clone()], &old_comments);
diff.load_current(&[sym], &new_comments);
let result = diff.compute_diff();
assert_eq!(result.stats.modified_count, 1);
}
#[test]
fn test_diff_generate_report() {
let mut diff = X86DocDiff::new();
let old: Vec<X86DocSymbolEntry> = vec![X86DocSymbolEntry {
qualified_name: "removed".to_string(),
name: "removed".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let new: Vec<X86DocSymbolEntry> = vec![X86DocSymbolEntry {
qualified_name: "added".to_string(),
name: "added".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
}];
let comments = HashMap::new();
diff.load_baseline(&old, &comments);
diff.load_current(&new, &comments);
let report = diff.generate_report();
assert!(report.contains("added"));
assert!(report.contains("removed"));
}
#[test]
fn test_diff_default() {
let diff = X86DocDiff::default();
assert!(diff.baseline.is_empty());
assert!(diff.current.is_empty());
}
#[test]
fn test_diff_change_type_display() {
assert_eq!(X86DiffChangeType::BriefChanged.to_string(), "brief-changed");
assert_eq!(X86DiffChangeType::BodyChanged.to_string(), "body-changed");
assert_eq!(X86DiffChangeType::DocAdded.to_string(), "doc-added");
assert_eq!(X86DiffChangeType::DocRemoved.to_string(), "doc-removed");
assert_eq!(
X86DiffChangeType::ParamsChanged.to_string(),
"params-changed"
);
assert_eq!(
X86DiffChangeType::ReturnChanged.to_string(),
"return-changed"
);
}
#[test]
fn test_index_builder_new() {
let builder = X86DocIndexBuilder::new();
assert!(builder.index_names);
assert!(builder.index_briefs);
assert!(builder.enable_stemming);
assert_eq!(builder.entry_count(), 0);
}
#[test]
fn test_index_builder_validate() {
let builder = X86DocIndexBuilder::new();
assert!(builder.validate().is_ok());
}
#[test]
fn test_index_builder_add_entry() {
let mut builder = X86DocIndexBuilder::new();
let sym = X86DocSymbolEntry {
qualified_name: "test_func".to_string(),
name: "test_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("A test function for indexing".to_string()),
file: None,
line: None,
parent: None,
link: "func_test_func.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![("param1".to_string(), "int".to_string())],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
assert_eq!(builder.entry_count(), 1);
assert!(builder.token_count() > 0);
}
#[test]
fn test_index_builder_search_exact() {
let mut builder = X86DocIndexBuilder::new();
let sym = X86DocSymbolEntry {
qualified_name: "searchable_function".to_string(),
name: "searchable_function".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("This function does something important".to_string()),
file: None,
line: None,
parent: None,
link: "sf.html".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("searchable");
assert!(!results.is_empty());
assert_eq!(results[0].name, "searchable_function");
}
#[test]
fn test_index_builder_search_no_match() {
let mut builder = X86DocIndexBuilder::new();
let sym = X86DocSymbolEntry {
qualified_name: "only_func".to_string(),
name: "only_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("nonexistent");
assert!(results.is_empty());
}
#[test]
fn test_index_builder_search_empty_query() {
let mut builder = X86DocIndexBuilder::new();
let sym = X86DocSymbolEntry {
qualified_name: "fn".to_string(),
name: "fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("");
assert!(results.is_empty());
}
#[test]
fn test_index_builder_with_comment() {
let mut builder = X86DocIndexBuilder::new();
let sym = X86DocSymbolEntry {
qualified_name: "documented_func".to_string(),
name: "documented_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
let comment = X86DocComment::from_line_doc(
"/// \\brief Initialize the system.\n/// \\details Sets up all necessary subsystems.",
);
builder.add_to_index(&sym, Some(&comment));
let results = builder.search("initialize");
assert!(!results.is_empty());
}
#[test]
fn test_index_builder_stop_words_filtered() {
let mut builder = X86DocIndexBuilder::new();
builder.remove_stop_words = true;
let sym = X86DocSymbolEntry {
qualified_name: "the_func".to_string(),
name: "the_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: Some("the is are was were be".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("func");
assert!(!results.is_empty());
}
#[test]
fn test_index_builder_stemming() {
let mut builder = X86DocIndexBuilder::new();
builder.enable_stemming = true;
let sym = X86DocSymbolEntry {
qualified_name: "running_func".to_string(),
name: "running_func".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("run");
assert!(!results.is_empty());
}
#[test]
fn test_index_builder_multiple_entries_same_token() {
let mut builder = X86DocIndexBuilder::new();
for i in 0..3 {
let sym = X86DocSymbolEntry {
qualified_name: format!("shared_fn_{}", i),
name: format!("shared_fn_{}", i),
kind: X86DocSymbolKind::Function,
brief: Some("shared functionality".to_string()),
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
}
let results = builder.search("shared");
assert_eq!(results.len(), 3);
}
#[test]
fn test_index_builder_default() {
let builder = X86DocIndexBuilder::default();
assert!(builder.index_names);
}
#[test]
fn test_index_builder_params_indexed() {
let mut builder = X86DocIndexBuilder::new();
builder.index_params = true;
let sym = X86DocSymbolEntry {
qualified_name: "param_fn".to_string(),
name: "param_fn".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![("unique_param_name".to_string(), "int".to_string())],
deprecated: false,
since: None,
};
builder.add_to_index(&sym, None);
let results = builder.search("unique_param_name");
assert!(!results.is_empty());
}
#[test]
fn test_stop_word_common() {
assert!(is_stop_word("the"));
assert!(is_stop_word("is"));
assert!(is_stop_word("and"));
assert!(is_stop_word("of"));
}
#[test]
fn test_stop_word_not_common() {
assert!(!is_stop_word("function"));
assert!(!is_stop_word("return"));
assert!(!is_stop_word("value"));
}
#[test]
fn test_simple_stem_ing() {
assert_eq!(simple_stem("running"), "runn");
assert_eq!(simple_stem("testing"), "test");
}
#[test]
fn test_simple_stem_ed() {
assert_eq!(simple_stem("tested"), "test");
}
#[test]
fn test_simple_stem_short_word() {
assert_eq!(simple_stem("run"), "run");
assert_eq!(simple_stem("test"), "test");
}
#[test]
fn test_simple_stem_tion() {
assert_eq!(simple_stem("computation"), "compu");
}
#[test]
fn test_simple_stem_ly() {
assert_eq!(simple_stem("quickly"), "quick");
}
#[test]
fn test_html_generate_index_page_has_all_elements() {
let mut html = X86DocHTML::new("FullTest", PathBuf::from("doc"));
let page = X86DocPage {
title: "Classes".to_string(),
kind: X86DocPageKind::Class,
output_file: PathBuf::from("classes.html"),
breadcrumb: vec![],
body_html: "<p>class content</p>".to_string(),
symbols: vec![],
sub_pages: vec![],
};
html.add_page(page);
let index_html = html.render_index_page();
assert!(index_html.contains("FullTest"));
assert!(index_html.contains("All Symbols"));
assert!(index_html.contains("class"));
}
#[test]
fn test_html_search_page_rendered() {
let html = X86DocHTML::new("P", PathBuf::from("doc"));
let search_html = html.render_search_page();
assert!(search_html.contains("search"));
assert!(search_html.contains("search-input"));
}
#[test]
fn test_html_function_page_with_comment() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
let params = vec![("input".to_string(), "const char*".to_string())];
let comment = X86DocComment::from_line_doc(
"/// \\brief Process input.\n/// \\param input The string to process.\n/// \\return Processed length.\n/// \\note Thread-safe.\n/// \\warning Input must be NUL-terminated.",
);
let result = html.render_function_page(
"process",
"size_t process(const char* input)",
"size_t",
¶ms,
Some("Process input string"),
Some(&comment),
);
assert!(result.contains("Process input"));
assert!(result.contains("Thread-safe"));
assert!(result.contains("Warning"));
}
#[test]
fn test_html_project_brief() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.project_brief = Some("A brief project description".to_string());
let page = X86DocPage {
title: "T".to_string(),
kind: X86DocPageKind::Index,
output_file: PathBuf::from("t.html"),
breadcrumb: vec![],
body_html: "<p>t</p>".to_string(),
symbols: vec![],
sub_pages: vec![],
};
let rendered = html.render_page(&page);
assert!(rendered.contains("A brief project description"));
}
#[test]
fn test_html_custom_css() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.custom_css = Some("/* custom */\nbody { margin: 0; }".to_string());
let css = html.build_css();
assert!(css.contains("/* custom */"));
assert!(css.contains("margin: 0"));
}
#[test]
fn test_html_defaults() {
let html = X86DocHTML::default();
assert!(html.generate_search);
assert!(html.responsive);
assert!(html.dark_mode);
}
#[test]
fn test_coverage_empty_is_100_percent() {
let cov = X86DocCoverage::new();
assert_eq!(cov.overall_coverage_pct(), 100.0);
}
#[test]
fn test_coverage_unknown_param_warning_includes_names() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// \\param z The z param.");
cov.record_symbol(
"fn",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&["x".to_string()],
);
let warnings = cov.get_warnings();
let unknown = warnings
.iter()
.find(|w| matches!(w, X86DocWarning::UnknownParam(_, _)));
assert!(unknown.is_some());
if let Some(X86DocWarning::UnknownParam(sym, param)) = unknown {
assert_eq!(sym, "fn");
assert_eq!(param, "z");
}
}
#[test]
fn test_coverage_deprecated_warning_in_list() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc("/// \\deprecated");
cov.record_symbol(
"f",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&[],
);
let warnings = cov.get_warnings();
assert!(warnings
.iter()
.any(|w| matches!(w, X86DocWarning::DeprecatedSymbol)));
}
#[test]
fn test_coverage_missing_brief_warning() {
let mut cov = X86DocCoverage::new();
let c = X86DocComment::from_line_doc(
"/// Some long detailed description without a brief command.",
);
cov.record_symbol(
"f",
X86DocSymbolKind::Function,
Some("f.c"),
None,
None,
Some(&c),
&[],
);
}
#[test]
fn test_doc_output_default_construction() {
let output = X86DocOutput::default();
assert!(output.html_files.is_empty());
assert!(output.markdown_content.is_none());
assert!(output.man_pages.is_empty());
assert!(output.xml_files.is_empty());
assert!(output.coverage_report.is_none());
assert!(output.validation_messages.is_empty());
}
#[test]
fn test_generator_add_class_with_empty_members() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
let comment = X86DocComment::from_line_doc("/// Empty class");
r#gen.add_class(
"Empty",
"Empty",
X86DocSymbolKind::Class,
Some("An empty class"),
&[],
&[],
&[],
None,
Some(&comment),
);
}
#[test]
fn test_generator_with_disabled_formats() {
let mut r#gen = X86DocGenerator::new("P", PathBuf::from("doc"));
r#gen.validate_output = false;
r#gen.enable_html = false;
r#gen.enable_markdown = false;
r#gen.enable_man = false;
r#gen.enable_xml = false;
r#gen.enable_coverage = false;
let sym = X86DocSymbolEntry {
qualified_name: "f".to_string(),
name: "f".to_string(),
kind: X86DocSymbolKind::Function,
brief: None,
file: None,
line: None,
parent: None,
link: "".to_string(),
access: None,
template_params: vec![],
signature: None,
return_type: None,
parameters: vec![],
deprecated: false,
since: None,
};
let c = X86DocComment::from_line_doc("/// f");
r#gen.add_function(&sym, Some(&c));
let result = r#gen.generate_all(&[sym], &HashMap::new());
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.html_files.is_empty());
assert!(output.man_pages.is_empty());
assert!(output.xml_files.is_empty());
assert!(output.coverage_report.is_none());
}
#[test]
fn test_html_generation_preserves_custom_js() {
let mut html = X86DocHTML::new("P", PathBuf::from("doc"));
html.custom_js = Some("console.log('hello');".to_string());
assert_eq!(html.custom_js.unwrap(), "console.log('hello');");
}
#[test]
fn test_xml_namespace_with_multiple_contents() {
let xml = X86DocXML::new("P", PathBuf::from("x"));
let contents = vec![
("class".to_string(), "ClassA".to_string()),
("class".to_string(), "ClassB".to_string()),
("function".to_string(), "FuncC".to_string()),
];
let result = xml.generate_namespace_xml("ns_N", "N", Some("Namespace"), &contents);
assert!(result.contains("ClassA"));
assert!(result.contains("ClassB"));
assert!(result.contains("FuncC"));
}
#[test]
fn test_comment_does_not_panic_on_malformed() {
let raw = "/// \\param";
let c = X86DocComment::from_line_doc(raw);
assert!(!c.is_empty);
let raw2 = "/// \\retval";
let c2 = X86DocComment::from_line_doc(raw2);
assert!(!c2.is_empty);
let raw3 = "/// \\tparam";
let c3 = X86DocComment::from_line_doc(raw3);
assert!(!c3.is_empty);
}
#[test]
fn test_all_structures_have_impl_default() {
let _html = X86DocHTML::default();
let _md = X86DocMarkdown::default();
let _man = X86DocMan::default();
let _xml = X86DocXML::default();
let _cov = X86DocCoverage::default();
let _gen = X86DocGenerator::default();
let _json = X86DocJSON::default();
let _diff = X86DocDiff::default();
let _builder = X86DocIndexBuilder::default();
let _comment = X86DocComment::default();
let _output = X86DocOutput::default();
}
#[test]
fn test_large_documentation_set() {
let mut r#gen = X86DocGenerator::new("LargeTest", PathBuf::from("doc"));
r#gen.validate_output = false;
let mut symbols: Vec<X86DocSymbolEntry> = Vec::new();
for i in 0..50 {
symbols.push(X86DocSymbolEntry {
qualified_name: format!("ns::func_{}", i),
name: format!("func_{}", i),
kind: X86DocSymbolKind::Function,
brief: Some(format!("Function number {}", i)),
file: Some("large_file.h".to_string()),
line: Some(i * 10),
parent: Some("ns".to_string()),
link: format!("func_{}.html", i),
access: None,
template_params: vec![],
signature: Some(format!("int func_{}(int x)", i)),
return_type: Some("int".to_string()),
parameters: vec![("x".to_string(), "int".to_string())],
deprecated: i % 10 == 0,
since: if i % 7 == 0 {
Some(format!("v{}.0", i))
} else {
None
},
});
}
let mut comments = HashMap::new();
for i in 0..50 {
if i % 2 == 0 {
comments.insert(
format!("ns::func_{}", i),
X86DocComment::from_line_doc(&format!(
"/// \\brief Function {}.\n/// \\param x The input parameter.\n/// \\return Computed value.",
i
)),
);
}
}
for sym in &symbols {
let c = comments.get(&sym.qualified_name as &str);
r#gen.add_function(sym, c);
}
let result = r#gen.generate_all(&symbols, &comments);
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.man_pages.len(), 50);
let coverage = output.coverage_report.unwrap();
assert!(coverage.contains("%"));
}
#[test]
fn test_coverage_warning_types_all_covered() {
let warnings = vec![
X86DocWarning::Undocumented,
X86DocWarning::MissingBrief,
X86DocWarning::MissingParamDoc("param1".to_string()),
X86DocWarning::MissingReturnDoc,
X86DocWarning::MissingTParamDoc("T".to_string()),
X86DocWarning::DeprecatedSymbol,
X86DocWarning::UnknownParam("symbol".to_string(), "bad_param".to_string()),
X86DocWarning::LowCoverage("scope".to_string(), 30.0),
];
for w in warnings {
assert!(!w.to_string().is_empty());
}
}
#[test]
fn test_generator_target_constant() {
assert_eq!(X86_DOC_TARGET, "x86_64-unknown-linux-gnu");
}
#[test]
fn test_generator_page_alignment() {
assert_eq!(X86_DOC_PAGE_ALIGNMENT, 4096);
}
#[test]
fn test_generator_max_recursion() {
assert_eq!(X86_DOC_MAX_RECURSION, 32);
}
#[test]
fn test_generator_max_brief_len() {
assert_eq!(X86_DOC_MAX_BRIEF_LEN, 256);
}
}