234 const char *mode_name,
const char *output_path,
235 const char *brief_description) {
238 (void)brief_description;
241 return SET_ERRNO(ERROR_INVALID_PARAM,
"config is required for man page generation");
245 bool should_close =
false;
247 if (output_path && strlen(output_path) > 0 && strcmp(output_path,
"-") != 0) {
250 if (stat(output_path, &st) == 0) {
252 log_plain(
"Man page file already exists: %s", output_path);
256 log_plain(
"Man page generation cancelled.");
257 return SET_ERRNO(ERROR_FILE_OPERATION,
"User cancelled overwrite");
260 log_plain(
"Overwriting existing man page file...");
265 return SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to open output file: %s", output_path);
273 manpage_resources_t resources;
274 memset(&resources, 0,
sizeof(resources));
276 if (err != ASCIICHAT_OK) {
286 return SET_ERRNO(ERROR_CONFIG,
"Man page resources are not valid");
290 const char *template_content = resources.template_content;
291 const char *p = template_content;
293 bool in_auto_section =
false;
294 char current_auto_section[128] =
"";
295 bool found_section_header =
false;
298 bool in_merge_section =
false;
299 bool merge_content_generated =
false;
300 char current_merge_section[64] = {0};
303 const char **manual_env_vars = NULL;
304 const char **manual_env_descs = NULL;
305 size_t manual_env_count = 0;
306 size_t manual_env_capacity = 0;
310 const char *line_end = strchr(p,
'\n');
313 if (!in_auto_section && !in_merge_section) {
319 size_t line_len = (size_t)(line_end - p);
322 bool has_merge_start =
false;
323 if (strstr(p,
"MERGE-START:") != NULL && strstr(p,
"MERGE-START:") < line_end) {
324 has_merge_start =
true;
327 if (has_merge_start) {
328 in_merge_section =
true;
329 merge_content_generated =
false;
330 manual_env_count = 0;
331 manual_env_capacity = 0;
334 const char *section_start = strstr(p,
"MERGE-START:");
335 if (section_start && section_start < line_end) {
336 section_start += strlen(
"MERGE-START:");
337 while (*section_start && section_start < line_end && isspace(*section_start))
340 const char *section_name_end = section_start;
341 while (section_name_end < line_end && *section_name_end && *section_name_end !=
'\n') {
345 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
349 size_t section_name_len = (size_t)(section_name_end - section_start);
350 if (section_name_len > 0 && section_name_len <
sizeof(current_merge_section)) {
351 SAFE_STRNCPY(current_merge_section, section_start, section_name_len);
352 current_merge_section[section_name_len] =
'\0';
361 if (strcmp(current_merge_section,
"ENVIRONMENT") == 0) {
362 const char *search_line = line_end + 1;
363 bool found_header =
false;
364 while (*search_line && !found_header) {
365 const char *search_line_end = strchr(search_line,
'\n');
366 if (!search_line_end)
370 if (strncmp(search_line,
".SH ", 4) == 0) {
372 size_t header_len = (size_t)(search_line_end - search_line);
373 fwrite(search_line, 1, header_len + 1, f);
374 p = search_line_end + 1;
380 if (strncmp(search_line,
".\\\" ", 4) == 0) {
381 search_line = search_line_end + 1;
397 }
else if (in_merge_section && strstr(p,
"MERGE-END:") != NULL && strstr(p,
"MERGE-END:") < line_end) {
399 if (!merge_content_generated) {
400 if (strcmp(current_merge_section,
"ENVIRONMENT") == 0) {
401 log_debug(
"[MANPAGE] Generating ENVIRONMENT with %zu manual + %zu auto variables", manual_env_count,
402 config->num_descriptors);
404 manual_env_count, manual_env_descs);
405 if (env_content && *env_content !=
'\0') {
406 log_debug(
"[MANPAGE] Writing ENVIRONMENT content: %zu bytes", strlen(env_content));
407 fprintf(f,
"%s", env_content);
409 log_warn(
"[MANPAGE] ENVIRONMENT content is empty!");
413 merge_content_generated =
true;
416 in_merge_section =
false;
418 memset(current_merge_section, 0,
sizeof(current_merge_section));
421 for (
size_t i = 0; i < manual_env_count; i++) {
422 if (manual_env_vars && manual_env_vars[i]) {
423 char *var_name = (
char *)manual_env_vars[i];
426 if (manual_env_descs && manual_env_descs[i]) {
427 char *var_desc = (
char *)manual_env_descs[i];
431 if (manual_env_vars) {
432 SAFE_FREE(manual_env_vars);
433 manual_env_vars = NULL;
435 if (manual_env_descs) {
436 SAFE_FREE(manual_env_descs);
437 manual_env_descs = NULL;
439 manual_env_count = 0;
440 manual_env_capacity = 0;
445 }
else if (in_merge_section) {
447 if (strcmp(current_merge_section,
"ENVIRONMENT") == 0) {
449 bool is_tp_marker = (line_len >= 3 && strncmp(p,
".TP", 3) == 0 && (line_len == 3 || isspace(p[3])));
450 bool is_b_marker = (line_len >= 3 && strncmp(p,
".B ", 3) == 0);
454 const char *var_start = p + 3;
455 while (*var_start && var_start < line_end && isspace(*var_start))
458 const char *var_end = var_start;
459 while (var_end < line_end && *var_end && *var_end !=
'\n') {
463 size_t var_len = (size_t)(var_end - var_start);
465 char *var_name = SAFE_MALLOC(var_len + 1,
char *);
466 SAFE_STRNCPY(var_name, var_start, var_len);
467 var_name[var_len] =
'\0';
470 while (var_len > 0 && isspace(var_name[var_len - 1])) {
471 var_name[--var_len] =
'\0';
475 if (manual_env_count >= manual_env_capacity) {
476 manual_env_capacity = manual_env_capacity == 0 ? 16 : manual_env_capacity * 2;
478 SAFE_REALLOC(manual_env_vars, manual_env_capacity *
sizeof(
const char *),
const char **);
480 SAFE_REALLOC(manual_env_descs, manual_env_capacity *
sizeof(
const char *),
const char **);
483 manual_env_vars[manual_env_count] = var_name;
484 manual_env_descs[manual_env_count] = NULL;
487 }
else if (manual_env_count > 0 && !is_tp_marker && !is_b_marker) {
490 const char *desc_start = p;
491 while (*desc_start && desc_start < line_end && isspace(*desc_start))
494 size_t desc_len = (size_t)(line_end - desc_start);
495 if (desc_len > 0 && manual_env_descs[manual_env_count - 1] == NULL) {
496 char *desc = SAFE_MALLOC(desc_len + 1,
char *);
497 SAFE_STRNCPY(desc, desc_start, desc_len);
498 desc[desc_len] =
'\0';
501 while (desc_len > 0 && isspace(desc[desc_len - 1])) {
502 desc[--desc_len] =
'\0';
506 manual_env_descs[manual_env_count - 1] = desc;
518 fwrite(p, 1, line_len + 1, f);
525 bool has_auto_start =
false;
526 const char *temp = p;
527 while (temp < line_end) {
528 if (strstr(temp,
"AUTO-START:") != NULL) {
529 const char *found = strstr(temp,
"AUTO-START:");
530 if (found < line_end) {
531 has_auto_start =
true;
541 if (has_auto_start) {
542 in_auto_section =
true;
543 found_section_header =
false;
546 const char *section_start = strstr(p,
"AUTO-START:");
547 if (section_start && section_start < line_end) {
548 section_start += strlen(
"AUTO-START:");
549 while (*section_start && section_start < line_end && isspace(*section_start))
552 const char *section_name_end = section_start;
554 while (section_name_end < line_end && *section_name_end && *section_name_end !=
'\n') {
559 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
563 size_t section_name_len = (size_t)(section_name_end - section_start);
564 if (section_name_len > 0 && section_name_len <
sizeof(current_auto_section)) {
565 SAFE_STRNCPY(current_auto_section, section_start, section_name_len);
566 current_auto_section[section_name_len] =
'\0';
572 }
else if (strstr(p,
"AUTO-END:") != NULL && strstr(p,
"AUTO-END:") < line_end) {
573 in_auto_section =
false;
575 memset(current_auto_section, 0,
sizeof(current_auto_section));
576 found_section_header =
false;
578 }
else if (in_auto_section) {
580 if (!found_section_header && strstr(p,
".\\\"") != NULL && strstr(p,
".\\\"") < line_end) {
582 if (strstr(p,
"auto-generated") == NULL) {
584 fwrite(p, 1, line_len + 1, f);
587 }
else if (!found_section_header && strstr(p,
".SH ") != NULL && strstr(p,
".SH ") < line_end) {
589 fwrite(p, 1, line_len + 1, f);
590 found_section_header =
true;
593 if (strcmp(current_auto_section,
"SYNOPSIS") == 0) {
594 log_debug(
"[MANPAGE] Generating SYNOPSIS section");
595 char *synopsis_content = NULL;
596 size_t synopsis_len = 0;
598 log_debug(
"[MANPAGE] SYNOPSIS: err=%d, len=%zu", gen_err, synopsis_len);
599 if (gen_err == ASCIICHAT_OK && synopsis_content && synopsis_len > 0) {
600 fprintf(f,
"%s", synopsis_content);
603 }
else if (strcmp(current_auto_section,
"POSITIONAL ARGUMENTS") == 0) {
604 log_debug(
"[MANPAGE] Generating POSITIONAL ARGUMENTS (config has %zu args)", config->num_positional_args);
607 size_t pos_len = strlen(pos_content);
608 log_debug(
"[MANPAGE] POSITIONAL ARGUMENTS: %zu bytes", pos_len);
609 if (*pos_content !=
'\0') {
610 fprintf(f,
"%s", pos_content);
614 }
else if (strcmp(current_auto_section,
"USAGE") == 0) {
615 log_debug(
"[MANPAGE] Generating USAGE (config has %zu usage lines)", config->num_usage_lines);
616 char *usage_content = NULL;
617 size_t usage_len = 0;
619 log_debug(
"[MANPAGE] USAGE: err=%d, len=%zu", usage_err, usage_len);
620 if (usage_err == ASCIICHAT_OK && usage_content && usage_len > 0) {
621 fprintf(f,
"%s", usage_content);
624 }
else if (strcmp(current_auto_section,
"EXAMPLES") == 0) {
625 log_debug(
"[MANPAGE] Generating EXAMPLES (config has %zu examples)", config->num_examples);
627 if (examples_content) {
628 size_t ex_len = strlen(examples_content);
629 log_debug(
"[MANPAGE] EXAMPLES: %zu bytes", ex_len);
630 if (*examples_content !=
'\0') {
631 fprintf(f,
"%s", examples_content);
635 }
else if (strcmp(current_auto_section,
"OPTIONS") == 0) {
636 log_debug(
"[MANPAGE] Generating OPTIONS (config has %zu descriptors)", config->num_descriptors);
638 if (options_content) {
639 size_t opt_len = strlen(options_content);
640 log_debug(
"[MANPAGE] OPTIONS: %zu bytes", opt_len);
641 if (*options_content !=
'\0') {
642 fprintf(f,
"%s", options_content);
653 bool is_marker_comment = (strstr(p,
".\\\" AUTO-") != NULL && strstr(p,
".\\\" AUTO-") < line_end) ||
654 (strstr(p,
".\\\" MANUAL-") != NULL && strstr(p,
".\\\" MANUAL-") < line_end) ||
655 (strstr(p,
".\\\" MERGE-") != NULL && strstr(p,
".\\\" MERGE-") < line_end);
656 if (!is_marker_comment) {
657 fwrite(p, 1, line_len + 1, f);
672 log_debug(
"Generated merged man page to %s", output_path ? output_path :
"stdout");