235 {
236 (void)mode_name;
237 (void)program_name;
238 (void)brief_description;
239
240 if (!config) {
241 return SET_ERRNO(ERROR_INVALID_PARAM, "config is required for man page generation");
242 }
243
244 FILE *f = NULL;
245 bool should_close = false;
246
247 if (output_path && strlen(output_path) > 0 && strcmp(output_path, "-") != 0) {
248
249 struct stat st;
250 if (stat(output_path, &st) == 0) {
251
252 log_plain("Man page file already exists: %s", output_path);
253
255 if (!overwrite) {
256 log_plain("Man page generation cancelled.");
257 return SET_ERRNO(ERROR_FILE_OPERATION, "User cancelled overwrite");
258 }
259
260 log_plain("Overwriting existing man page file...");
261 }
262
264 if (!f) {
265 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open output file: %s", output_path);
266 }
267 should_close = true;
268 } else {
269 f = stdout;
270 }
271
272
273 manpage_resources_t resources;
274 memset(&resources, 0, sizeof(resources));
276 if (err != ASCIICHAT_OK) {
277 if (should_close)
278 fclose(f);
279 return err;
280 }
281
283 if (should_close)
284 fclose(f);
286 return SET_ERRNO(ERROR_CONFIG, "Man page resources are not valid");
287 }
288
289
290 const char *template_content = resources.template_content;
291 const char *p = template_content;
292
293 bool in_auto_section = false;
294 char current_auto_section[128] = "";
295 bool found_section_header = false;
296
297
298 bool in_merge_section = false;
299 bool merge_content_generated = false;
300 char current_merge_section[64] = {0};
301
302
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;
307
308 while (*p) {
309
310 const char *line_end = strchr(p, '\n');
311 if (!line_end) {
312
313 if (!in_auto_section && !in_merge_section) {
314 fputs(p, f);
315 }
316 break;
317 }
318
319 size_t line_len = (size_t)(line_end - p);
320
321
322 bool has_merge_start = false;
323 if (strstr(p, "MERGE-START:") != NULL && strstr(p, "MERGE-START:") < line_end) {
324 has_merge_start = true;
325 }
326
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;
332
333
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))
338 section_start++;
339
340 const char *section_name_end = section_start;
341 while (section_name_end < line_end && *section_name_end && *section_name_end != '\n') {
342 section_name_end++;
343 }
344
345 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
346 section_name_end--;
347 }
348
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';
353 }
354 }
355
356
357
358
359
360
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)
367 break;
368
369
370 if (strncmp(search_line, ".SH ", 4) == 0) {
371
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;
375 found_header = true;
376 break;
377 }
378
379
380 if (strncmp(search_line, ".\\\" ", 4) == 0) {
381 search_line = search_line_end + 1;
382 continue;
383 }
384
385
386 break;
387 }
388 if (found_header) {
389 continue;
390 }
391 }
392
393
394 p = line_end + 1;
395 continue;
396
397 } else if (in_merge_section && strstr(p, "MERGE-END:") != NULL && strstr(p, "MERGE-END:") < line_end) {
398
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);
408 } else {
409 log_warn("[MANPAGE] ENVIRONMENT content is empty!");
410 }
412 }
413 merge_content_generated = true;
414 }
415
416 in_merge_section = false;
417
418 memset(current_merge_section, 0, sizeof(current_merge_section));
419
420
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];
424 SAFE_FREE(var_name);
425 }
426 if (manual_env_descs && manual_env_descs[i]) {
427 char *var_desc = (char *)manual_env_descs[i];
428 SAFE_FREE(var_desc);
429 }
430 }
431 if (manual_env_vars) {
432 SAFE_FREE(manual_env_vars);
433 manual_env_vars = NULL;
434 }
435 if (manual_env_descs) {
436 SAFE_FREE(manual_env_descs);
437 manual_env_descs = NULL;
438 }
439 manual_env_count = 0;
440 manual_env_capacity = 0;
441
442 p = line_end + 1;
443 continue;
444
445 } else if (in_merge_section) {
446
447 if (strcmp(current_merge_section, "ENVIRONMENT") == 0) {
448
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);
451
452 if (is_b_marker) {
453
454 const char *var_start = p + 3;
455 while (*var_start && var_start < line_end && isspace(*var_start))
456 var_start++;
457
458 const char *var_end = var_start;
459 while (var_end < line_end && *var_end && *var_end != '\n') {
460 var_end++;
461 }
462
463 size_t var_len = (size_t)(var_end - var_start);
464 if (var_len > 0) {
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';
468
469
470 while (var_len > 0 && isspace(var_name[var_len - 1])) {
471 var_name[--var_len] = '\0';
472 }
473
474
475 if (manual_env_count >= manual_env_capacity) {
476 manual_env_capacity = manual_env_capacity == 0 ? 16 : manual_env_capacity * 2;
477 manual_env_vars =
478 SAFE_REALLOC(manual_env_vars, manual_env_capacity * sizeof(const char *), const char **);
479 manual_env_descs =
480 SAFE_REALLOC(manual_env_descs, manual_env_capacity * sizeof(const char *), const char **);
481 }
482
483 manual_env_vars[manual_env_count] = var_name;
484 manual_env_descs[manual_env_count] = NULL;
485 manual_env_count++;
486 }
487 } else if (manual_env_count > 0 && !is_tp_marker && !is_b_marker) {
488
489
490 const char *desc_start = p;
491 while (*desc_start && desc_start < line_end && isspace(*desc_start))
492 desc_start++;
493
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';
499
500
501 while (desc_len > 0 && isspace(desc[desc_len - 1])) {
502 desc[--desc_len] = '\0';
503 }
504
505 if (desc_len > 0) {
506 manual_env_descs[manual_env_count - 1] = desc;
507 } else {
508 SAFE_FREE(desc);
509 }
510 }
511 }
512
513
514 p = line_end + 1;
515 continue;
516 } else {
517
518 fwrite(p, 1, line_len + 1, f);
519 p = line_end + 1;
520 continue;
521 }
522 }
523
524
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;
532 break;
533 }
534
535 temp = found + 1;
536 } else {
537 break;
538 }
539 }
540
541 if (has_auto_start) {
542 in_auto_section = true;
543 found_section_header = false;
544
545
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))
550 section_start++;
551
552 const char *section_name_end = section_start;
553
554 while (section_name_end < line_end && *section_name_end && *section_name_end != '\n') {
555 section_name_end++;
556 }
557
558
559 while (section_name_end > section_start && isspace(*(section_name_end - 1))) {
560 section_name_end--;
561 }
562
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';
567 }
568 }
569
570
571
572 } else if (strstr(p, "AUTO-END:") != NULL && strstr(p, "AUTO-END:") < line_end) {
573 in_auto_section = false;
574
575 memset(current_auto_section, 0, sizeof(current_auto_section));
576 found_section_header = false;
577
578 } else if (in_auto_section) {
579
580 if (!found_section_header && strstr(p, ".\\\"") != NULL && strstr(p, ".\\\"") < line_end) {
581
582 if (strstr(p, "auto-generated") == NULL) {
583
584 fwrite(p, 1, line_len + 1, f);
585 }
586
587 } else if (!found_section_header && strstr(p, ".SH ") != NULL && strstr(p, ".SH ") < line_end) {
588
589 fwrite(p, 1, line_len + 1, f);
590 found_section_header = true;
591
592
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);
602 }
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);
606 if (pos_content) {
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);
611 }
612 }
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);
623 }
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);
632 }
633 }
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);
643 }
644 }
646 }
647 }
648
649 } else {
650
651
652
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);
658 }
659 }
660
661
662 p = line_end + 1;
663 }
664
666
667 fflush(f);
668 if (should_close) {
669 fclose(f);
670 }
671
672 log_debug("Generated merged man page to %s", output_path ? output_path : "stdout");
673 return ASCIICHAT_OK;
674}
char * manpage_content_generate_environment_with_manual(const options_config_t *config, const char **manual_vars, size_t manual_count, const char **manual_descs)
void manpage_content_free_environment(char *content)
char * manpage_content_generate_examples(const options_config_t *config)
void manpage_content_free_examples(char *content)
char * manpage_content_generate_options(const options_config_t *config)
void manpage_content_free_options(char *content)
asciichat_error_t manpage_merger_generate_usage(const options_config_t *config, char **out_content, size_t *out_len)
void manpage_merger_free_content(char *content)
asciichat_error_t manpage_merger_generate_synopsis(const char *mode_name, char **out_content, size_t *out_len)
void manpage_content_free_positional(char *content)
char * manpage_content_generate_positional(const options_config_t *config)
void manpage_resources_destroy(manpage_resources_t *resources)
asciichat_error_t manpage_resources_load(manpage_resources_t *resources)
bool manpage_resources_is_valid(const manpage_resources_t *resources)
bool platform_prompt_yes_no(const char *question, bool default_yes)
FILE * platform_fopen(const char *filename, const char *mode)