mirror of
https://github.com/pound-emu/ballistic.git
synced 2026-01-31 01:15:21 +01:00
While I do not like the Rust Language as a whole, their documentation generator is the best I've ever seen. in any language. I want to implement something like it for Ballistic. Like I said in the README, I have absolutely zero motivation to create a documentation generator so `cdoc.c` is made completely with AI. The code is messy but the generated HTML files look beautiful. Signed-off-by: Ronald Caesar <github43132@proton.me>
884 lines
34 KiB
C
884 lines
34 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <signal.h>
|
|
#include <dirent.h>
|
|
#include <clang-c/Index.h>
|
|
#include <cmark.h>
|
|
|
|
// --- 1. DATA STRUCTURES ---
|
|
|
|
typedef enum {
|
|
KIND_FUNCTION,
|
|
KIND_STRUCT,
|
|
KIND_ENUM,
|
|
KIND_TYPEDEF
|
|
} ItemKind;
|
|
|
|
typedef struct {
|
|
char* name;
|
|
char* type; // For Structs: type name. For Enums: value.
|
|
char* doc;
|
|
} Field;
|
|
|
|
typedef struct {
|
|
char* name;
|
|
char* doc_comment;
|
|
ItemKind kind;
|
|
char* return_type;
|
|
char* underlying_type;
|
|
Field* args;
|
|
Field* fields;
|
|
int arg_count;
|
|
int field_count;
|
|
char* source_file;
|
|
char* anchor_id;
|
|
} DocItem;
|
|
|
|
typedef struct {
|
|
DocItem* items;
|
|
size_t count;
|
|
size_t capacity;
|
|
char* filename;
|
|
char* file_doc;
|
|
} FileContext;
|
|
|
|
typedef struct {
|
|
FileContext* files;
|
|
size_t count;
|
|
size_t capacity;
|
|
DocItem** registry;
|
|
size_t reg_count;
|
|
size_t reg_cap;
|
|
} ProjectContext;
|
|
|
|
// --- 2. HELPERS ---
|
|
|
|
char* my_strdup(const char* s) {
|
|
if (!s) return NULL;
|
|
size_t len = strlen(s);
|
|
char* d = malloc(len + 1);
|
|
if (d) memcpy(d, s, len + 1);
|
|
return d;
|
|
}
|
|
|
|
char* to_cstr(CXString cx_str) {
|
|
const char* temp = clang_getCString(cx_str);
|
|
char* result = temp ? my_strdup(temp) : my_strdup("");
|
|
clang_disposeString(cx_str);
|
|
return result;
|
|
}
|
|
|
|
const char* get_filename(const char* path) {
|
|
const char* last_slash = strrchr(path, '/');
|
|
const char* last_backslash = strrchr(path, '\\');
|
|
const char* filename = path;
|
|
if (last_slash && last_slash > filename) filename = last_slash + 1;
|
|
if (last_backslash && last_backslash > filename) filename = last_backslash + 1;
|
|
return filename;
|
|
}
|
|
|
|
void init_project(ProjectContext* proj) {
|
|
proj->count = 0;
|
|
proj->capacity = 10;
|
|
proj->files = malloc(sizeof(FileContext) * proj->capacity);
|
|
proj->reg_count = 0;
|
|
proj->reg_cap = 100;
|
|
proj->registry = malloc(sizeof(DocItem*) * proj->reg_cap);
|
|
}
|
|
|
|
FileContext* add_file(ProjectContext* proj, const char* filepath) {
|
|
if (proj->count >= proj->capacity) {
|
|
proj->capacity *= 2;
|
|
proj->files = realloc(proj->files, sizeof(FileContext) * proj->capacity);
|
|
}
|
|
FileContext* ctx = &proj->files[proj->count++];
|
|
ctx->filename = my_strdup(get_filename(filepath));
|
|
ctx->file_doc = NULL;
|
|
ctx->count = 0;
|
|
ctx->capacity = 32;
|
|
ctx->items = malloc(sizeof(DocItem) * ctx->capacity);
|
|
return ctx;
|
|
}
|
|
|
|
void register_item(ProjectContext* proj, DocItem* item) {
|
|
if (proj->reg_count >= proj->reg_cap) {
|
|
proj->reg_cap *= 2;
|
|
proj->registry = realloc(proj->registry, sizeof(DocItem*) * proj->reg_cap);
|
|
}
|
|
proj->registry[proj->reg_count++] = item;
|
|
}
|
|
|
|
DocItem* add_item(FileContext* ctx) {
|
|
if (ctx->count >= ctx->capacity) {
|
|
ctx->capacity *= 2;
|
|
ctx->items = realloc(ctx->items, sizeof(DocItem) * ctx->capacity);
|
|
}
|
|
DocItem* item = &ctx->items[ctx->count++];
|
|
memset(item, 0, sizeof(DocItem));
|
|
item->source_file = ctx->filename;
|
|
return item;
|
|
}
|
|
|
|
int item_exists(FileContext* ctx, const char* name) {
|
|
if (!name) return 0;
|
|
for (size_t i = 0; i < ctx->count; i++) {
|
|
if (ctx->items[i].name && strcmp(ctx->items[i].name, name) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// --- 3. LINK RESOLUTION ---
|
|
// --- 3. LINK RESOLUTION ---
|
|
|
|
// NEW: Helper to find any symbol (Item, Enum Variant, or Struct.Field)
|
|
// Returns 1 if found, setting out_file and out_anchor. 0 otherwise.
|
|
int find_link_target(ProjectContext* proj, const char* name, const char** out_file, const char** out_anchor) {
|
|
if (!name) return 0;
|
|
|
|
for (size_t i = 0; i < proj->reg_count; i++) {
|
|
DocItem* item = proj->registry[i];
|
|
|
|
// 1. Check Top-Level Item Name (Struct, Enum, Function, Typedef)
|
|
if (item->name && strcmp(item->name, name) == 0) {
|
|
*out_file = item->source_file;
|
|
*out_anchor = item->anchor_id;
|
|
return 1;
|
|
}
|
|
|
|
// 2. Check Enum Variants (Global Scope in C)
|
|
// Allows linking to [`SOME_ENUM_VALUE`]
|
|
if (item->kind == KIND_ENUM) {
|
|
for (int j = 0; j < item->field_count; j++) {
|
|
if (item->fields[j].name && strcmp(item->fields[j].name, name) == 0) {
|
|
*out_file = item->source_file;
|
|
*out_anchor = item->fields[j].name; // Anchor is the variant name
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Check Struct Fields (Scoped: StructName.FieldName)
|
|
// Allows linking to [`MyStruct.my_field`]
|
|
if (item->kind == KIND_STRUCT && item->name) {
|
|
size_t len = strlen(item->name);
|
|
if (strncmp(name, item->name, len) == 0 && name[len] == '.') {
|
|
const char* field_part = name + len + 1;
|
|
for (int j = 0; j < item->field_count; j++) {
|
|
if (item->fields[j].name && strcmp(item->fields[j].name, field_part) == 0) {
|
|
*out_file = item->source_file;
|
|
*out_anchor = item->fields[j].name;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Kept for internal logic if needed, but mostly replaced by find_link_target
|
|
DocItem* find_item(ProjectContext* proj, const char* name) {
|
|
if (!name) return NULL;
|
|
for (size_t i = 0; i < proj->reg_count; i++) {
|
|
if (proj->registry[i]->name && strcmp(proj->registry[i]->name, name) == 0) {
|
|
return proj->registry[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int is_ident_char(char c) {
|
|
return isalnum((unsigned char)c) || c == '_';
|
|
}
|
|
|
|
char* linkify_type(ProjectContext* proj, const char* raw_type, const char* current_file) {
|
|
if (!raw_type) return NULL;
|
|
|
|
size_t cap = strlen(raw_type) * 3 + 256;
|
|
char* out = malloc(cap);
|
|
size_t out_len = 0;
|
|
out[0] = '\0';
|
|
|
|
const char* p = raw_type;
|
|
char word[256];
|
|
int w_idx = 0;
|
|
|
|
while (*p) {
|
|
if (is_ident_char(*p)) {
|
|
if (w_idx < 255) word[w_idx++] = *p;
|
|
} else {
|
|
if (w_idx > 0) {
|
|
word[w_idx] = '\0';
|
|
|
|
const char *target_file, *target_anchor;
|
|
if (find_link_target(proj, word, &target_file, &target_anchor)) {
|
|
// Check if same file for relative link
|
|
if (current_file && strcmp(target_file, current_file) == 0) {
|
|
out_len += snprintf(out + out_len, cap - out_len,
|
|
"<a class='type' href='#%s'>%s</a>",
|
|
target_anchor, word);
|
|
} else {
|
|
out_len += snprintf(out + out_len, cap - out_len,
|
|
"<a class='type' href='%s.html#%s'>%s</a>",
|
|
target_file, target_anchor, word);
|
|
}
|
|
} else {
|
|
out_len += snprintf(out + out_len, cap - out_len, "%s", word);
|
|
}
|
|
w_idx = 0;
|
|
}
|
|
if (out_len < cap - 1) {
|
|
out[out_len++] = *p;
|
|
out[out_len] = '\0';
|
|
}
|
|
}
|
|
p++;
|
|
}
|
|
|
|
if (w_idx > 0) {
|
|
word[w_idx] = '\0';
|
|
const char *target_file, *target_anchor;
|
|
if (find_link_target(proj, word, &target_file, &target_anchor)) {
|
|
if (current_file && strcmp(target_file, current_file) == 0) {
|
|
out_len += snprintf(out + out_len, cap - out_len,
|
|
"<a class='type' href='#%s'>%s</a>",
|
|
target_anchor, word);
|
|
} else {
|
|
out_len += snprintf(out + out_len, cap - out_len,
|
|
"<a class='type' href='%s.html#%s'>%s</a>",
|
|
target_file, target_anchor, word);
|
|
}
|
|
} else {
|
|
out_len += snprintf(out + out_len, cap - out_len, "%s", word);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
char* resolve_links(ProjectContext* proj, const char* text, const char* current_file) {
|
|
if (!text) return NULL;
|
|
size_t cap = strlen(text) * 2 + 512;
|
|
char* output = malloc(cap);
|
|
size_t out_len = 0;
|
|
const char* p = text;
|
|
|
|
while (*p) {
|
|
if (p[0] == '[' && p[1] == '`') {
|
|
const char* start = p + 2;
|
|
const char* end = strstr(start, "`]");
|
|
if (end) {
|
|
int name_len = end - start;
|
|
char name[128];
|
|
if (name_len < 127) {
|
|
strncpy(name, start, name_len);
|
|
name[name_len] = '\0';
|
|
|
|
const char *target_file, *target_anchor;
|
|
if (find_link_target(proj, name, &target_file, &target_anchor)) {
|
|
if (current_file && strcmp(target_file, current_file) == 0) {
|
|
out_len += sprintf(output + out_len, "[`%s`](#%s)", name, target_anchor);
|
|
} else {
|
|
out_len += sprintf(output + out_len, "[`%s`](%s.html#%s)", name, target_file, target_anchor);
|
|
}
|
|
p = end + 2;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
output[out_len++] = *p++;
|
|
if (out_len >= cap - 100) {
|
|
cap *= 2;
|
|
output = realloc(output, cap);
|
|
}
|
|
}
|
|
output[out_len] = '\0';
|
|
return output;
|
|
}
|
|
|
|
char* clean_comment(const char* raw) {
|
|
if (!raw) return NULL;
|
|
size_t len = strlen(raw);
|
|
char* output = malloc(len + 1);
|
|
char* out_ptr = output;
|
|
char* temp = my_strdup(raw);
|
|
char* line = strtok(temp, "\n");
|
|
|
|
while (line != NULL) {
|
|
char* p = line;
|
|
while (*p && isspace((unsigned char)*p)) p++;
|
|
if (strncmp(p, "/**", 3) == 0) p += 3;
|
|
else if (strncmp(p, "/*!", 3) == 0) p += 3;
|
|
else if (strncmp(p, "///", 3) == 0) p += 3;
|
|
else if (strncmp(p, "*/", 2) == 0) { line = strtok(NULL, "\n"); continue; }
|
|
else if (*p == '*') p++;
|
|
if (*p == ' ') p++;
|
|
while (*p) *out_ptr++ = *p++;
|
|
*out_ptr++ = '\n';
|
|
line = strtok(NULL, "\n");
|
|
}
|
|
*out_ptr = '\0';
|
|
free(temp);
|
|
return output;
|
|
}
|
|
|
|
// --- 4. PARSING ---
|
|
|
|
void parse_file_level_docs(FileContext* ctx, const char* real_path) {
|
|
FILE* f = fopen(real_path, "r");
|
|
if (!f) return;
|
|
char* buffer = malloc(50000);
|
|
if (!buffer) { fclose(f); return; }
|
|
buffer[0] = '\0';
|
|
char line[2048];
|
|
while (fgets(line, sizeof(line), f)) {
|
|
char* p = line;
|
|
while (*p && isspace((unsigned char)*p)) p++;
|
|
if (strncmp(p, "//!", 3) == 0) {
|
|
p += 3; if (*p == ' ') p++;
|
|
strcat(buffer, p);
|
|
}
|
|
}
|
|
fclose(f);
|
|
if (strlen(buffer) > 0) ctx->file_doc = buffer;
|
|
else free(buffer);
|
|
}
|
|
|
|
int is_skippable(CXCursor cursor) {
|
|
if (clang_Cursor_isAnonymous(cursor)) return 1;
|
|
CXString cx = clang_getCursorSpelling(cursor);
|
|
const char* s = clang_getCString(cx);
|
|
int skip = (!s || strlen(s) == 0 || strstr(s, "(unnamed") != NULL);
|
|
clang_disposeString(cx);
|
|
return skip;
|
|
}
|
|
|
|
enum CXChildVisitResult struct_visitor(CXCursor cursor, CXCursor parent, CXClientData client_data) {
|
|
DocItem* item = (DocItem*)client_data;
|
|
if (clang_getCursorKind(cursor) == CXCursor_FieldDecl) {
|
|
int idx = item->field_count++;
|
|
item->fields = realloc(item->fields, sizeof(Field) * item->field_count);
|
|
item->fields[idx].name = to_cstr(clang_getCursorSpelling(cursor));
|
|
item->fields[idx].type = to_cstr(clang_getTypeSpelling(clang_getCursorType(cursor)));
|
|
item->fields[idx].doc = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
}
|
|
return CXChildVisit_Continue;
|
|
}
|
|
|
|
enum CXChildVisitResult enum_visitor(CXCursor cursor, CXCursor parent, CXClientData client_data) {
|
|
DocItem* item = (DocItem*)client_data;
|
|
if (clang_getCursorKind(cursor) == CXCursor_EnumConstantDecl) {
|
|
int idx = item->field_count++;
|
|
item->fields = realloc(item->fields, sizeof(Field) * item->field_count);
|
|
item->fields[idx].name = to_cstr(clang_getCursorSpelling(cursor));
|
|
long long val = clang_getEnumConstantDeclValue(cursor);
|
|
char val_str[64];
|
|
snprintf(val_str, 64, "%lld", val);
|
|
item->fields[idx].type = my_strdup(val_str);
|
|
item->fields[idx].doc = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
}
|
|
return CXChildVisit_Continue;
|
|
}
|
|
|
|
enum CXChildVisitResult typedef_param_visitor(CXCursor cursor, CXCursor parent, CXClientData client_data) {
|
|
DocItem* item = (DocItem*)client_data;
|
|
if (clang_getCursorKind(cursor) == CXCursor_ParmDecl) {
|
|
int idx = item->arg_count++;
|
|
item->args = realloc(item->args, sizeof(Field) * item->arg_count);
|
|
item->args[idx].name = to_cstr(clang_getCursorSpelling(cursor));
|
|
item->args[idx].type = to_cstr(clang_getTypeSpelling(clang_getCursorType(cursor)));
|
|
item->args[idx].doc = NULL;
|
|
}
|
|
return CXChildVisit_Continue;
|
|
}
|
|
|
|
enum CXChildVisitResult main_visitor(CXCursor cursor, CXCursor parent, CXClientData client_data) {
|
|
void** args = (void**)client_data;
|
|
FileContext* ctx = (FileContext*)args[0];
|
|
ProjectContext* proj = (ProjectContext*)args[1];
|
|
|
|
CXSourceLocation location = clang_getCursorLocation(cursor);
|
|
if (clang_Location_isFromMainFile(location) == 0) return CXChildVisit_Continue;
|
|
|
|
enum CXCursorKind kind = clang_getCursorKind(cursor);
|
|
DocItem* item = NULL;
|
|
|
|
if (kind == CXCursor_FunctionDecl) {
|
|
char* name = to_cstr(clang_getCursorSpelling(cursor));
|
|
if (item_exists(ctx, name)) { free(name); return CXChildVisit_Continue; }
|
|
|
|
item = add_item(ctx);
|
|
item->kind = KIND_FUNCTION;
|
|
item->name = name;
|
|
item->doc_comment = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
item->return_type = to_cstr(clang_getTypeSpelling(clang_getCursorResultType(cursor)));
|
|
|
|
char buf[256];
|
|
snprintf(buf, 256, "fn.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
|
|
int num = clang_Cursor_getNumArguments(cursor);
|
|
item->arg_count = num;
|
|
item->args = malloc(sizeof(Field) * num);
|
|
for (int i=0; i<num; i++) {
|
|
CXCursor arg = clang_Cursor_getArgument(cursor, i);
|
|
item->args[i].name = to_cstr(clang_getCursorSpelling(arg));
|
|
item->args[i].type = to_cstr(clang_getTypeSpelling(clang_getCursorType(arg)));
|
|
item->args[i].doc = NULL;
|
|
}
|
|
}
|
|
else if (kind == CXCursor_StructDecl) {
|
|
if (is_skippable(cursor) || !clang_isCursorDefinition(cursor)) return CXChildVisit_Continue;
|
|
|
|
char* name = to_cstr(clang_getCursorSpelling(cursor));
|
|
if (item_exists(ctx, name)) { free(name); return CXChildVisit_Continue; }
|
|
|
|
item = add_item(ctx);
|
|
item->kind = KIND_STRUCT;
|
|
item->name = name;
|
|
item->doc_comment = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
|
|
char buf[256];
|
|
snprintf(buf, 256, "struct.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
|
|
clang_visitChildren(cursor, struct_visitor, item);
|
|
}
|
|
else if (kind == CXCursor_EnumDecl) {
|
|
if (is_skippable(cursor) || !clang_isCursorDefinition(cursor)) return CXChildVisit_Continue;
|
|
|
|
char* name = to_cstr(clang_getCursorSpelling(cursor));
|
|
if (item_exists(ctx, name)) { free(name); return CXChildVisit_Continue; }
|
|
|
|
item = add_item(ctx);
|
|
item->kind = KIND_ENUM;
|
|
item->name = name;
|
|
item->doc_comment = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
|
|
char buf[256];
|
|
snprintf(buf, 256, "enum.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
|
|
clang_visitChildren(cursor, enum_visitor, item);
|
|
}
|
|
else if (kind == CXCursor_TypedefDecl) {
|
|
char* name = to_cstr(clang_getCursorSpelling(cursor));
|
|
if (item_exists(ctx, name)) { free(name); return CXChildVisit_Continue; }
|
|
|
|
item = add_item(ctx);
|
|
item->name = name;
|
|
|
|
CXType underlying = clang_getTypedefDeclUnderlyingType(cursor);
|
|
CXType canonical = clang_getCanonicalType(underlying);
|
|
|
|
if (canonical.kind == CXType_Record) {
|
|
item->kind = KIND_STRUCT;
|
|
char buf[256];
|
|
snprintf(buf, 256, "struct.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
|
|
char* doc = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
if (!doc || !*doc) {
|
|
if(doc) free(doc);
|
|
CXCursor sc = clang_getTypeDeclaration(canonical);
|
|
doc = to_cstr(clang_Cursor_getRawCommentText(sc));
|
|
}
|
|
item->doc_comment = doc;
|
|
|
|
CXCursor sc = clang_getTypeDeclaration(canonical);
|
|
clang_visitChildren(sc, struct_visitor, item);
|
|
}
|
|
else if (canonical.kind == CXType_Enum) {
|
|
item->kind = KIND_ENUM;
|
|
char buf[256];
|
|
snprintf(buf, 256, "enum.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
|
|
char* doc = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
if (!doc || !*doc) {
|
|
if(doc) free(doc);
|
|
CXCursor sc = clang_getTypeDeclaration(canonical);
|
|
doc = to_cstr(clang_Cursor_getRawCommentText(sc));
|
|
}
|
|
item->doc_comment = doc;
|
|
|
|
CXCursor sc = clang_getTypeDeclaration(canonical);
|
|
clang_visitChildren(sc, enum_visitor, item);
|
|
}
|
|
else {
|
|
item->kind = KIND_TYPEDEF;
|
|
char buf[256];
|
|
snprintf(buf, 256, "type.%s", item->name ? item->name : "unknown");
|
|
item->anchor_id = my_strdup(buf);
|
|
item->doc_comment = to_cstr(clang_Cursor_getRawCommentText(cursor));
|
|
item->underlying_type = to_cstr(clang_getTypeSpelling(underlying));
|
|
|
|
// UPDATED: Check if it's a function pointer and extract params
|
|
if (underlying.kind == CXType_Pointer) {
|
|
CXType pointee = clang_getPointeeType(underlying);
|
|
if (pointee.kind == CXType_FunctionProto) {
|
|
item->return_type = to_cstr(clang_getTypeSpelling(clang_getResultType(pointee)));
|
|
// Visit children to find ParmDecl parameters
|
|
clang_visitChildren(cursor, typedef_param_visitor, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item) register_item(proj, item);
|
|
|
|
return CXChildVisit_Recurse;
|
|
}
|
|
|
|
// --- 5. HTML GENERATION ---
|
|
|
|
void render_md(FILE* f, ProjectContext* proj, const char* raw, const char* current_file) {
|
|
if (!raw) return;
|
|
char* clean = clean_comment(raw);
|
|
char* linked = resolve_links(proj, clean, current_file);
|
|
char* html = cmark_markdown_to_html(linked, strlen(linked), CMARK_OPT_UNSAFE);
|
|
fprintf(f, "%s", html);
|
|
free(html);
|
|
free(linked);
|
|
free(clean);
|
|
}
|
|
|
|
void write_common_head(FILE* f, const char* title) {
|
|
fprintf(f, "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>");
|
|
fprintf(f, "<title>%s</title>", title);
|
|
fprintf(f, "<link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500&family=Source+Code+Pro:wght@400;600&family=Source+Serif+4:wght@400;600;700&display=swap'>");
|
|
fprintf(f, "<style>");
|
|
fprintf(f, ":root { --bg: #0f1419; --sidebar-bg: #14191f; --text: #c5c5c5; --link: #39afd7; --code-bg: #191f26; --border: #252c37; --header-text: #fff; }");
|
|
fprintf(f, "body { font-family: 'Source Serif 4', serif; font-size: 16px; background: var(--bg); color: var(--text); margin: 0; display: flex; height: 100vh; overflow: hidden; line-height: 1.6; }");
|
|
fprintf(f, ".sidebar { width: 250px; background: var(--sidebar-bg); border-right: 1px solid var(--border); overflow-y: auto; padding: 20px; flex-shrink: 0; }");
|
|
fprintf(f, ".main { flex: 1; padding: 40px; overflow-y: auto; max-width: 960px; margin: 0 auto; }");
|
|
fprintf(f, ".sidebar a { display: block; color: var(--text); text-decoration: none; font-family: 'Fira Sans', sans-serif; font-size: 14px; margin: 6px 0; }");
|
|
fprintf(f, ".sidebar a:hover { color: var(--link); background: #222; border-radius: 3px; }");
|
|
fprintf(f, ".sidebar h3 { font-family: 'Fira Sans'; font-size: 14px; color: #fff; margin-top: 20px; text-transform: uppercase; font-weight: 500; }");
|
|
fprintf(f, "h1 { font-size: 28px; color: var(--header-text); margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }");
|
|
fprintf(f, "h2 { font-size: 24px; color: var(--header-text); margin-top: 50px; border-bottom: 1px solid var(--border); padding-bottom: 5px; font-weight: 600; }");
|
|
fprintf(f, "h3 { font-size: 20px; color: var(--header-text); margin-top: 30px; margin-bottom: 15px; font-weight: 600; }");
|
|
fprintf(f, "a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }");
|
|
fprintf(f, "pre { width: 100%%; box-sizing: border-box; background: var(--code-bg); padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 14px; line-height: 1.5; border: 1px solid var(--border); }");
|
|
fprintf(f, "code { font-family: 'Source Code Pro', monospace; background: var(--code-bg); padding: 0.1em 0.3em; border-radius: 4px; font-size: 0.875em; }");
|
|
fprintf(f, ".item-decl { width: 100%%; box-sizing: border-box; background: var(--code-bg); padding: 15px; font-family: 'Source Code Pro'; margin-bottom: 20px; border-radius: 6px; white-space: pre-wrap; overflow-x: auto; font-size: 14px; line-height: 1.5; color: #e6e6e6; border: 1px solid var(--border); }");
|
|
fprintf(f, ".item-decl a.type { color: #79c0ff; text-decoration: none; border-bottom: 1px dotted #555; }");
|
|
fprintf(f, ".item-decl a.type:hover { border-bottom: 1px solid #79c0ff; }");
|
|
fprintf(f, ".kw { color: #ff7b72; font-weight: bold; }");
|
|
fprintf(f, ".type { color: #79c0ff; }");
|
|
fprintf(f, ".fn { color: #d2a8ff; font-weight: bold; }");
|
|
fprintf(f, ".lit { color: #a5d6ff; }");
|
|
fprintf(f, ".field-item { margin-bottom: 15px; }");
|
|
fprintf(f, ".field-name { font-family: 'Source Code Pro', monospace; font-size: 16px; font-weight: 600; color: #fff; background: var(--code-bg); padding: 2px 6px; border-radius: 4px; display: inline-block; }");
|
|
fprintf(f, ".field-doc * { margin-top: 6px; margin-left: 10px; color: #ccc; font-size: 16px; line-height: 1.5; }");
|
|
fprintf(f, ".field-doc { margin: 0; }");
|
|
fprintf(f, ".docblock { margin-top: 10px; margin-bottom: 30px; font-size: 16px; }");
|
|
fprintf(f, ".docblock h1 { font-size: 18px; font-weight: 600; margin-top: 25px; margin-bottom: 10px; border-bottom: none; color: var(--header-text); }");
|
|
fprintf(f, ".docblock h2 { font-size: 17px; font-weight: 600; margin-top: 25px; margin-bottom: 10px; border-bottom: none; color: var(--header-text); }");
|
|
fprintf(f, ".docblock h3 { font-size: 16px; font-weight: 600; margin-top: 20px; margin-bottom: 10px; }");
|
|
fprintf(f, ".docblock p { margin-bottom: 1em; }");
|
|
fprintf(f, ".docblock ul { padding-left: 20px; margin-bottom: 1em; }");
|
|
fprintf(f, "</style></head><body>");
|
|
}
|
|
|
|
void render_sidebar_section(FILE* f, FileContext* ctx, ItemKind kind, const char* title) {
|
|
int found = 0;
|
|
// Check if any items of this kind exist
|
|
for(size_t i=0; i<ctx->count; i++) {
|
|
if (ctx->items[i].kind == kind) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
// If found, print the header and the links
|
|
if (found) {
|
|
fprintf(f, "<h3>%s</h3>", title);
|
|
for(size_t i=0; i<ctx->count; i++) {
|
|
if (ctx->items[i].kind == kind) {
|
|
fprintf(f, "<a href='#%s'>%s</a>", ctx->items[i].anchor_id, ctx->items[i].name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void generate_file_html(ProjectContext* proj, FileContext* ctx, const char* out_dir) {
|
|
char path[1024];
|
|
snprintf(path, 1024, "%s/%s.html", out_dir, ctx->filename);
|
|
FILE* f = fopen(path, "w");
|
|
if (!f) return;
|
|
|
|
write_common_head(f, ctx->filename);
|
|
|
|
// --- SIDEBAR GENERATION ---
|
|
fprintf(f, "<nav class='sidebar'>");
|
|
fprintf(f, "<a href='index.html' style='font-size: 18px; font-weight: bold; margin-bottom: 20px;'>Back to Index</a>");
|
|
fprintf(f, "<div style='font-weight: bold; color: #fff; margin-bottom: 10px;'>%s</div>", ctx->filename);
|
|
|
|
// CHANGED: Group items by kind in the sidebar
|
|
render_sidebar_section(f, ctx, KIND_STRUCT, "Structs");
|
|
render_sidebar_section(f, ctx, KIND_ENUM, "Enums");
|
|
render_sidebar_section(f, ctx, KIND_FUNCTION, "Functions");
|
|
render_sidebar_section(f, ctx, KIND_TYPEDEF, "Type Aliases");
|
|
|
|
fprintf(f, "</nav>");
|
|
// --------------------------
|
|
|
|
fprintf(f, "<main class='main'>");
|
|
fprintf(f, "<h1>Header <span class='fn'>%s</span></h1>", ctx->filename);
|
|
|
|
if (ctx->file_doc) {
|
|
fprintf(f, "<div class='docblock'>");
|
|
render_md(f, proj, ctx->file_doc, ctx->filename);
|
|
fprintf(f, "</div>");
|
|
}
|
|
|
|
for(size_t i=0; i<ctx->count; i++) {
|
|
DocItem* item = &ctx->items[i];
|
|
const char* kind_str = "Unknown";
|
|
if (item->kind == KIND_FUNCTION) kind_str = "Function";
|
|
else if (item->kind == KIND_STRUCT) kind_str = "Struct";
|
|
else if (item->kind == KIND_ENUM) kind_str = "Enum";
|
|
else if (item->kind == KIND_TYPEDEF) kind_str = "Type Alias";
|
|
|
|
fprintf(f, "<h2 id='%s'>%s <a href='#%s'>%s</a></h2>", item->anchor_id, kind_str, item->anchor_id, item->name);
|
|
|
|
fprintf(f, "<div class='item-decl'>");
|
|
if (item->kind == KIND_FUNCTION) {
|
|
char* ret_linked = linkify_type(proj, item->return_type, ctx->filename);
|
|
fprintf(f, "<span class='type'>%s</span> <span class='fn'>%s</span>(", ret_linked, item->name);
|
|
free(ret_linked);
|
|
|
|
for(int j=0; j<item->arg_count; j++) {
|
|
char* arg_linked = linkify_type(proj, item->args[j].type, ctx->filename);
|
|
fprintf(f, "\n <span class='type'>%s</span> %s", arg_linked, item->args[j].name);
|
|
free(arg_linked);
|
|
if(j<item->arg_count-1) fprintf(f, ",");
|
|
}
|
|
fprintf(f, "\n)");
|
|
}
|
|
else if (item->kind == KIND_STRUCT) {
|
|
fprintf(f, "<span class='kw'>struct</span> <span class='type'>%s</span> {", item->name);
|
|
for(int j=0; j<item->field_count; j++) {
|
|
char* f_linked = linkify_type(proj, item->fields[j].type, ctx->filename);
|
|
fprintf(f, "\n <span class='type'>%s</span> %s;", f_linked, item->fields[j].name);
|
|
free(f_linked);
|
|
}
|
|
fprintf(f, "\n}");
|
|
}
|
|
else if (item->kind == KIND_ENUM) {
|
|
fprintf(f, "<span class='kw'>enum</span> <span class='type'>%s</span> {", item->name);
|
|
for(int j=0; j<item->field_count; j++) {
|
|
fprintf(f, "\n %s = <span class='lit'>%s</span>,", item->fields[j].name, item->fields[j].type);
|
|
}
|
|
fprintf(f, "\n}");
|
|
}
|
|
else {
|
|
if (item->return_type && item->arg_count > 0) {
|
|
char* ret_linked = linkify_type(proj, item->return_type, ctx->filename);
|
|
fprintf(f, "<span class='kw'>typedef</span> %s = <span class='type'>%s</span> (*)(", item->name, ret_linked);
|
|
free(ret_linked);
|
|
|
|
for(int j=0; j<item->arg_count; j++) {
|
|
char* arg_linked = linkify_type(proj, item->args[j].type, ctx->filename);
|
|
fprintf(f, "<span class='type'>%s</span> %s", arg_linked, item->args[j].name);
|
|
free(arg_linked);
|
|
if(j < item->arg_count - 1) fprintf(f, ", ");
|
|
}
|
|
fprintf(f, ");");
|
|
} else {
|
|
char* under_linked = linkify_type(proj, item->underlying_type, ctx->filename);
|
|
fprintf(f, "<span class='kw'>typedef</span> %s = <span class='type'>%s</span>;", item->name, under_linked);
|
|
free(under_linked);
|
|
}
|
|
}
|
|
fprintf(f, "</div>");
|
|
|
|
if (item->doc_comment) {
|
|
fprintf(f, "<div class='docblock'>");
|
|
render_md(f, proj, item->doc_comment, ctx->filename);
|
|
fprintf(f, "</div>");
|
|
}
|
|
|
|
if ((item->kind == KIND_STRUCT || item->kind == KIND_ENUM) && item->field_count > 0) {
|
|
int has_field_docs = 0;
|
|
for(int k=0; k<item->field_count; k++) if(item->fields[k].doc) has_field_docs=1;
|
|
|
|
if(has_field_docs) {
|
|
fprintf(f, "<h3>%s</h3>", item->kind == KIND_ENUM ? "Variants" : "Fields");
|
|
for(int j=0; j<item->field_count; j++) {
|
|
if(item->fields[j].doc) {
|
|
fprintf(f, "<div id='%s' class='field-item'>", item->fields[j].name);
|
|
fprintf(f, "<code class='field-name'>%s</code>", item->fields[j].name);
|
|
fprintf(f, "<div class='field-doc'>");
|
|
render_md(f, proj, item->fields[j].doc, ctx->filename);
|
|
fprintf(f, "</div></div>");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fprintf(f, "</main></body></html>");
|
|
fclose(f);
|
|
}
|
|
|
|
void generate_index(ProjectContext* proj, const char* out_dir) {
|
|
char path[1024];
|
|
snprintf(path, 1024, "%s/index.html", out_dir);
|
|
FILE* f = fopen(path, "w");
|
|
if (!f) return;
|
|
|
|
write_common_head(f, "Project Documentation");
|
|
|
|
fprintf(f, "<nav class='sidebar'><h3>Files</h3>");
|
|
for(size_t i=0; i<proj->count; i++) {
|
|
fprintf(f, "<a href='%s.html'>%s</a>", proj->files[i].filename, proj->files[i].filename);
|
|
}
|
|
fprintf(f, "</nav>");
|
|
|
|
fprintf(f, "<main class='main'>");
|
|
fprintf(f, "<h1>Project Documentation</h1>");
|
|
fprintf(f, "<h2>Headers</h2><ul>");
|
|
for(size_t i=0; i<proj->count; i++) {
|
|
fprintf(f, "<li><a href='%s.html'>%s</a></li>", proj->files[i].filename, proj->files[i].filename);
|
|
}
|
|
fprintf(f, "</ul>");
|
|
|
|
fprintf(f, "<h2>Global Symbols</h2><div style='display:flex; flex-wrap:wrap; gap: 10px;'>");
|
|
for(size_t i=0; i<proj->reg_count; i++) {
|
|
DocItem* item = proj->registry[i];
|
|
if (item->name) {
|
|
fprintf(f, "<a href='%s.html#%s' style='background: #222; padding: 5px 10px; border-radius: 4px;'>%s</a>",
|
|
item->source_file, item->anchor_id, item->name);
|
|
}
|
|
}
|
|
fprintf(f, "</div>");
|
|
fprintf(f, "</main></body></html>");
|
|
fclose(f);
|
|
}
|
|
|
|
int dir_exists(const char* path) {
|
|
struct stat sb;
|
|
return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode);
|
|
}
|
|
|
|
int compare_items(const void* a, const void* b) {
|
|
const DocItem* da = (const DocItem*)a;
|
|
const DocItem* db = (const DocItem*)b;
|
|
|
|
// Safety checks for NULL names
|
|
if (!da->name && !db->name) return 0;
|
|
if (!da->name) return 1;
|
|
if (!db->name) return -1;
|
|
|
|
return strcmp(da->name, db->name);
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
#ifndef _WIN32
|
|
signal(SIGPIPE, SIG_IGN);
|
|
#endif
|
|
|
|
if (argc < 3) {
|
|
printf("Usage: %s <output_dir> <file1.h> [file2.h ...]\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
const char* out_dir = argv[1];
|
|
|
|
char clang_inc_path[1024] = {0};
|
|
int found_inc = 0;
|
|
FILE* p = popen("clang -print-resource-dir", "r");
|
|
if (p) {
|
|
if (fgets(clang_inc_path, sizeof(clang_inc_path), p)) {
|
|
size_t len = strlen(clang_inc_path);
|
|
while(len > 0 && isspace((unsigned char)clang_inc_path[len-1])) clang_inc_path[--len] = '\0';
|
|
strcat(clang_inc_path, "/include");
|
|
if (dir_exists(clang_inc_path)) found_inc = 1;
|
|
}
|
|
pclose(p);
|
|
}
|
|
|
|
if (!found_inc) {
|
|
const char* common_paths[] = {
|
|
"/usr/lib/clang/18/include", "/usr/lib/clang/17/include",
|
|
"/usr/lib/clang/16/include", "/usr/lib/clang/15/include",
|
|
"/usr/lib/clang/14/include", "/usr/lib64/clang/18/include",
|
|
NULL
|
|
};
|
|
for (int i = 0; common_paths[i]; i++) {
|
|
if (dir_exists(common_paths[i])) {
|
|
strcpy(clang_inc_path, common_paths[i]);
|
|
found_inc = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
char arg_include_flag[1100];
|
|
const char* clang_args[8];
|
|
int num_args = 0;
|
|
clang_args[num_args++] = "-I.";
|
|
clang_args[num_args++] = "-Iinclude";
|
|
clang_args[num_args++] = "-xc";
|
|
if (found_inc) {
|
|
snprintf(arg_include_flag, sizeof(arg_include_flag), "-I%s", clang_inc_path);
|
|
clang_args[num_args++] = arg_include_flag;
|
|
printf("Using Clang headers: %s\n", clang_inc_path);
|
|
}
|
|
|
|
ProjectContext proj;
|
|
init_project(&proj);
|
|
CXIndex index = clang_createIndex(0, 1);
|
|
|
|
for (int i = 2; i < argc; i++) {
|
|
const char* filepath = argv[i];
|
|
printf("Parsing %s...\n", filepath);
|
|
|
|
FileContext* file_ctx = add_file(&proj, filepath);
|
|
parse_file_level_docs(file_ctx, filepath);
|
|
|
|
CXTranslationUnit unit = clang_parseTranslationUnit(
|
|
index, filepath, clang_args, num_args, NULL, 0, CXTranslationUnit_None
|
|
);
|
|
|
|
if (!unit) {
|
|
printf("Failed to parse %s\n", filepath);
|
|
continue;
|
|
}
|
|
|
|
CXCursor root = clang_getTranslationUnitCursor(unit);
|
|
void* args[] = { file_ctx, &proj };
|
|
clang_visitChildren(root, main_visitor, args);
|
|
clang_disposeTranslationUnit(unit);
|
|
}
|
|
|
|
for (size_t i = 0; i < proj.count; i++) {
|
|
qsort(proj.files[i].items, proj.files[i].count, sizeof(DocItem), compare_items);
|
|
}
|
|
|
|
printf("Generating HTML in '%s'...\n", out_dir);
|
|
|
|
#ifdef _WIN32
|
|
_mkdir(out_dir);
|
|
#else
|
|
mkdir(out_dir, 0777);
|
|
#endif
|
|
|
|
for (size_t i = 0; i < proj.count; i++) {
|
|
generate_file_html(&proj, &proj.files[i], out_dir);
|
|
}
|
|
generate_index(&proj, out_dir);
|
|
|
|
clang_disposeIndex(index);
|
|
printf("Done! Open %s/index.html\n", out_dir);
|
|
return 0;
|
|
}
|