ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
options/manpage.c
Go to the documentation of this file.
1
14#include <ascii-chat/options/manpage.h>
15#include <ascii-chat/options/manpage/resources.h>
16#include <ascii-chat/options/manpage/parser.h>
17#include <ascii-chat/options/manpage/formatter.h>
18#include <ascii-chat/options/manpage/merger.h>
19#include <ascii-chat/options/manpage/content/options.h>
20#include <ascii-chat/options/manpage/content/environment.h>
21#include <ascii-chat/options/manpage/content/usage.h>
22#include <ascii-chat/options/manpage/content/examples.h>
23#include <ascii-chat/options/manpage/content/positional.h>
24#include <ascii-chat/options/builder.h>
25#include <ascii-chat/common.h>
26#include <ascii-chat/log/logging.h>
27#include <ascii-chat/platform/question.h>
28#include <ascii-chat/platform/stat.h>
29#include <ascii-chat/platform/util.h>
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33#include <ctype.h>
34
35// =============================================================================
36// Helper Functions (exported for content generators)
37// =============================================================================
38
39const char *escape_groff_special(const char *str) {
40 return str ? str : "";
41}
42
43const char *format_mode_names(option_mode_bitmask_t mode_bitmask) {
44 if (mode_bitmask == 0) {
45 return NULL;
46 }
47
48 if ((mode_bitmask & OPTION_MODE_BINARY) && !(mode_bitmask & 0x1F)) {
49 return "global";
50 }
51
52 option_mode_bitmask_t user_modes_mask = (1 << MODE_SERVER) | (1 << MODE_CLIENT) | (1 << MODE_MIRROR) |
53 (1 << MODE_DISCOVERY_SERVICE) | (1 << MODE_DISCOVERY);
54 if ((mode_bitmask & user_modes_mask) == user_modes_mask) {
55 // If all user modes are set, check if binary-level is also set
56 if (mode_bitmask & OPTION_MODE_BINARY) {
57 return "global, client, server, mirror, discovery-service";
58 }
59 return "all modes";
60 }
61
62 static char mode_str[256];
63 mode_str[0] = '\0';
64 int pos = 0;
65
66 // Add global if binary-level is set
67 if (mode_bitmask & OPTION_MODE_BINARY) {
68 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "global");
69 }
70
71 // List modes in order: default, client, server, mirror, discovery-service
72 if (mode_bitmask & (1 << MODE_DISCOVERY)) {
73 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sdefault", pos > 0 ? ", " : "");
74 }
75 if (mode_bitmask & (1 << MODE_CLIENT)) {
76 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sclient", pos > 0 ? ", " : "");
77 }
78 if (mode_bitmask & (1 << MODE_SERVER)) {
79 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sserver", pos > 0 ? ", " : "");
80 }
81 if (mode_bitmask & (1 << MODE_MIRROR)) {
82 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%smirror", pos > 0 ? ", " : "");
83 }
84 if (mode_bitmask & (1 << MODE_DISCOVERY_SERVICE)) {
85 pos += safe_snprintf(mode_str + pos, sizeof(mode_str) - pos, "%sdiscovery-service", pos > 0 ? ", " : "");
86 }
87
88 return pos > 0 ? mode_str : NULL;
89}
90
91// =============================================================================
92// Main Public API Functions
93// =============================================================================
94
95asciichat_error_t options_config_generate_manpage_template(const options_config_t *config, const char *program_name,
96 const char *mode_name, const char *output_path,
97 const char *brief_description) {
98 if (!config || !program_name || !brief_description) {
99 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing required parameters for man page generation");
100 }
101
102 FILE *f = NULL;
103 bool should_close = false;
104
105 if (output_path) {
106 f = platform_fopen(output_path, "w");
107 if (!f) {
108 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open output file: %s", output_path);
109 }
110 should_close = true;
111 } else {
112 f = stdout;
113 }
114
115 // Write title/header
116 manpage_fmt_write_title(f, program_name, mode_name, brief_description);
117
118 // Write SYNOPSIS
119 char *synopsis_content = NULL;
120 size_t synopsis_len = 0;
121 asciichat_error_t err = manpage_merger_generate_synopsis(mode_name, &synopsis_content, &synopsis_len);
122 if (err == ASCIICHAT_OK && synopsis_content && synopsis_len > 0) {
123 fprintf(f, "%s", synopsis_content);
124 manpage_merger_free_content(synopsis_content);
125 }
126
127 // Write POSITIONAL ARGUMENTS if present
128 if (config->num_positional_args > 0) {
129 char *pos_content = manpage_content_generate_positional(config);
130 if (pos_content && *pos_content != '\0') {
131 fprintf(f, "%s", pos_content);
132 }
134 }
135
136 // Write DESCRIPTION
137 manpage_fmt_write_section(f, "DESCRIPTION");
138 fprintf(f, ".B ascii-chat\nis a terminal-based video chat application that converts webcam video to ASCII\n");
139 fprintf(f, "art in real-time. It enables video chat directly in your terminal, whether you're\n");
140 fprintf(f, "using a local console, a remote SSH session, or any terminal emulator.\n");
142
143 // Write USAGE
144 char *usage_content = NULL;
145 size_t usage_len = 0;
146 err = manpage_merger_generate_usage(config, &usage_content, &usage_len);
147 if (err == ASCIICHAT_OK && usage_content && usage_len > 0) {
148 fprintf(f, ".SH USAGE\n%s", usage_content);
149 manpage_merger_free_content(usage_content);
150 }
151
152 // Write OPTIONS
153 if (config->num_descriptors > 0) {
154 char *options_content = manpage_content_generate_options(config);
155 if (options_content && *options_content != '\0') {
156 fprintf(f, "%s", options_content);
157 }
158 manpage_content_free_options(options_content);
159 }
160
161 // Write EXAMPLES if present
162 if (config->num_examples > 0) {
163 char *examples_content = manpage_content_generate_examples(config);
164 if (examples_content && *examples_content != '\0') {
165 fprintf(f, "%s", examples_content);
166 }
167 manpage_content_free_examples(examples_content);
168 }
169
170 // Write ENVIRONMENT if present
171 bool has_env_vars = false;
172 for (size_t i = 0; i < config->num_descriptors; i++) {
173 if (config->descriptors[i].env_var_name) {
174 has_env_vars = true;
175 break;
176 }
177 }
178
179 if (has_env_vars) {
180 char *env_content = manpage_content_generate_environment(config);
181 if (env_content && *env_content != '\0') {
182 fprintf(f, "%s", env_content);
183 }
185 }
186
187 // Write placeholder manual sections
188 manpage_fmt_write_section(f, "FILES");
189 fprintf(f, ".I ~/.ascii-chat/config.toml\n");
190 fprintf(f, "User configuration file\n");
192
193 manpage_fmt_write_section(f, "NOTES");
194 fprintf(f, "For more information and examples, visit the project repository.\n");
196
197 manpage_fmt_write_section(f, "BUGS");
198 fprintf(f, "Report bugs at the project issue tracker.\n");
200
201 manpage_fmt_write_section(f, "AUTHOR");
202 fprintf(f, "Contributed by the ascii-chat community.\n");
204
205 manpage_fmt_write_section(f, "SEE ALSO");
206 fprintf(f, ".B man(1),\n");
207 fprintf(f, ".B groff_man(7)\n");
209
210 if (should_close) {
211 fclose(f);
212 }
213
214 log_debug("Generated man page template to %s", output_path ? output_path : "stdout");
215 return ASCIICHAT_OK;
216}
217
218asciichat_error_t options_builder_generate_manpage_template(options_builder_t *builder, const char *program_name,
219 const char *mode_name, const char *output_path,
220 const char *brief_description) {
221 if (!builder || !program_name || !brief_description) {
222 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing required parameters");
223 }
224
225 const options_config_t *config = options_builder_build(builder);
226 if (!config) {
227 return SET_ERRNO(ERROR_CONFIG, "Failed to build options configuration");
228 }
229
230 return options_config_generate_manpage_template(config, program_name, mode_name, output_path, brief_description);
231}
232
233asciichat_error_t options_config_generate_manpage_merged(const options_config_t *config, const char *program_name,
234 const char *mode_name, const char *output_path,
235 const char *brief_description) {
236 (void)mode_name; // Not used
237 (void)program_name; // Not used
238 (void)brief_description; // Not used
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 // Check if file already exists and prompt for confirmation
249 struct stat st;
250 if (stat(output_path, &st) == 0) {
251 // File exists - ask user if they want to overwrite
252 log_plain("Man page file already exists: %s", output_path);
253
254 bool overwrite = platform_prompt_yes_no("Overwrite", false); // Default to No
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
263 f = platform_fopen(output_path, "w");
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 // Load template resources
273 manpage_resources_t resources;
274 memset(&resources, 0, sizeof(resources));
275 asciichat_error_t err = manpage_resources_load(&resources);
276 if (err != ASCIICHAT_OK) {
277 if (should_close)
278 fclose(f);
279 return err;
280 }
281
282 if (!manpage_resources_is_valid(&resources)) {
283 if (should_close)
284 fclose(f);
285 manpage_resources_destroy(&resources);
286 return SET_ERRNO(ERROR_CONFIG, "Man page resources are not valid");
287 }
288
289 // Process template and merge AUTO sections with generated content
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 // Track MERGE sections
298 bool in_merge_section = false;
299 bool merge_content_generated = false;
300 char current_merge_section[64] = {0};
301
302 // For ENVIRONMENT MERGE section: collect manual variables
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 // Find next line
310 const char *line_end = strchr(p, '\n');
311 if (!line_end) {
312 // Last line without newline
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 // Check for MERGE-START marker (only in current line)
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; // Reset manual variable collection
331 manual_env_capacity = 0;
332
333 // Extract section name (e.g., "ENVIRONMENT" from "MERGE-START: ENVIRONMENT")
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 // Do NOT write the MERGE-START marker line - it's an internal control marker
357 // The content will be generated when MERGE-END is encountered
358
359 // For ENVIRONMENT MERGE sections, find and preserve the .SH header
360 // (skipping over any marker comment lines that might be in between)
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 // Check if this is the .SH header
370 if (strncmp(search_line, ".SH ", 4) == 0) {
371 // Write the .SH header line
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 // Skip marker comment lines (.\" MANUAL-*, .\" MERGE-*, etc)
380 if (strncmp(search_line, ".\\\" ", 4) == 0) {
381 search_line = search_line_end + 1;
382 continue; // Keep searching
383 }
384
385 // If we hit a non-marker, non-.SH line, stop searching
386 break;
387 }
388 if (found_header) {
389 continue; // Skip the "Skip to next line" at line 359
390 }
391 }
392
393 // Skip to next line
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 // Before writing MERGE-END, generate content if not already done
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);
403 char *env_content = manpage_content_generate_environment_with_manual(config, manual_env_vars,
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 // Do NOT write the MERGE-END marker line - it's an internal control marker
418 memset(current_merge_section, 0, sizeof(current_merge_section));
419
420 // Free collected manual variables (strings first, then arrays)
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 // Within MERGE section: collect manual environment variables for ENVIRONMENT section
447 if (strcmp(current_merge_section, "ENVIRONMENT") == 0) {
448 // Check if line starts with ".TP" or ".B " (groff markers)
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 // Extract variable name after ".B "
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 // Trim trailing whitespace
470 while (var_len > 0 && isspace(var_name[var_len - 1])) {
471 var_name[--var_len] = '\0';
472 }
473
474 // Store the variable name
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; // Will be set from next line(s)
485 manual_env_count++;
486 }
487 } else if (manual_env_count > 0 && !is_tp_marker && !is_b_marker) {
488 // This is likely a description line following a ".B var_name" line
489 // Set description for the last collected variable
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 // Trim trailing whitespace
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 // For ENVIRONMENT MERGE sections, do NOT write template content
513 // All content will be generated at MERGE-END
514 p = line_end + 1;
515 continue;
516 } else {
517 // For other MERGE sections (if any), write template content as-is
518 fwrite(p, 1, line_len + 1, f);
519 p = line_end + 1;
520 continue;
521 }
522 }
523
524 // Check for AUTO-START marker (only in current line)
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 // strstr() found it but it's beyond this line, so keep looking
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 // Extract section name (e.g., "SYNOPSIS" from "AUTO-START: SYNOPSIS")
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 // Extract section name: everything until end of line, then trim trailing whitespace
554 while (section_name_end < line_end && *section_name_end && *section_name_end != '\n') {
555 section_name_end++;
556 }
557
558 // Now trim trailing whitespace from the section name
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 // Don't write the AUTO-START marker line - it's an internal control comment
571
572 } else if (strstr(p, "AUTO-END:") != NULL && strstr(p, "AUTO-END:") < line_end) {
573 in_auto_section = false;
574 // Don't write the AUTO-END marker line - it's an internal control comment
575 memset(current_auto_section, 0, sizeof(current_auto_section));
576 found_section_header = false;
577
578 } else if (in_auto_section) {
579 // Preserve comment lines that come before the section header
580 if (!found_section_header && strstr(p, ".\\\"") != NULL && strstr(p, ".\\\"") < line_end) {
581 // Skip comment lines in AUTO sections that contain "auto-generated" text
582 if (strstr(p, "auto-generated") == NULL) {
583 // Non-marker comment lines can be preserved
584 fwrite(p, 1, line_len + 1, f);
585 }
586 // Skip comment lines with "auto-generated" marker text
587 } else if (!found_section_header && strstr(p, ".SH ") != NULL && strstr(p, ".SH ") < line_end) {
588 // If this is the section header line (.SH ...), write it and generate content
589 fwrite(p, 1, line_len + 1, f);
590 found_section_header = true;
591
592 // Generate content for this AUTO section
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;
597 asciichat_error_t gen_err = manpage_merger_generate_synopsis(NULL, &synopsis_content, &synopsis_len);
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);
601 manpage_merger_free_content(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);
605 char *pos_content = manpage_content_generate_positional(config);
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;
618 asciichat_error_t usage_err = manpage_merger_generate_usage(config, &usage_content, &usage_len);
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);
622 manpage_merger_free_content(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);
626 char *examples_content = manpage_content_generate_examples(config);
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 }
634 manpage_content_free_examples(examples_content);
635 } else if (strcmp(current_auto_section, "OPTIONS") == 0) {
636 log_debug("[MANPAGE] Generating OPTIONS (config has %zu descriptors)", config->num_descriptors);
637 char *options_content = manpage_content_generate_options(config);
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 }
645 manpage_content_free_options(options_content);
646 }
647 }
648 // Otherwise skip old content between section header and AUTO-END
649 } else {
650 // Write all manual content (not in AUTO section)
651 // Skip marker comment lines: .\" AUTO-*, .\" MANUAL-*, .\" MERGE-*
652 // These are internal build-time control comments
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 // Move to next line
662 p = line_end + 1;
663 }
664
665 manpage_resources_destroy(&resources);
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}
675
676parsed_section_t *parse_manpage_sections(const char *filepath, size_t *num_sections) {
677 if (!filepath || !num_sections) {
678 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
679 return NULL;
680 }
681
682 FILE *f = platform_fopen(filepath, "r");
683 if (!f) {
684 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open file: %s", filepath);
685 return NULL;
686 }
687
688 parsed_section_t *sections = NULL;
689 asciichat_error_t err = manpage_parser_parse_file(f, &sections, num_sections);
690 fclose(f);
691
692 if (err != ASCIICHAT_OK) {
693 return NULL;
694 }
695
696 return sections;
697}
698
699void free_parsed_sections(parsed_section_t *sections, size_t num_sections) {
700 manpage_parser_free_sections(sections, num_sections);
701}
702
703const parsed_section_t *find_section(const parsed_section_t *sections, size_t num_sections, const char *section_name) {
704 return manpage_parser_find_section(sections, num_sections, section_name);
705}
706
707#ifndef NDEBUG
708asciichat_error_t options_config_generate_final_manpage(const char *template_path, const char *output_path,
709 const char *version_string, const char *content_file_path) {
710 (void)template_path;
711 (void)output_path;
712 (void)version_string;
713 (void)content_file_path;
714 return SET_ERRNO(ERROR_CONFIG, "Not implemented in refactored version");
715}
716#endif
717
options_config_t * options_builder_build(options_builder_t *builder)
Definition builder.c:452
char * manpage_content_generate_environment_with_manual(const options_config_t *config, const char **manual_vars, size_t manual_count, const char **manual_descs)
Definition environment.c:74
void manpage_content_free_environment(char *content)
char * manpage_content_generate_environment(const options_config_t *config)
Definition environment.c:14
char * manpage_content_generate_examples(const options_config_t *config)
Definition examples.c:79
void manpage_content_free_examples(char *content)
Definition examples.c:141
void manpage_fmt_write_title(FILE *f, const char *program_name, const char *mode_name, const char *brief_description)
Definition formatter.c:75
void manpage_fmt_write_section(FILE *f, const char *section_name)
Definition formatter.c:28
void manpage_fmt_write_blank_line(FILE *f)
Definition formatter.c:35
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)
Definition merger.c:79
void manpage_merger_free_content(char *content)
Definition merger.c:179
asciichat_error_t manpage_merger_generate_synopsis(const char *mode_name, char **out_content, size_t *out_len)
Definition merger.c:137
const char * format_mode_names(option_mode_bitmask_t mode_bitmask)
const parsed_section_t * find_section(const parsed_section_t *sections, size_t num_sections, const char *section_name)
const char * escape_groff_special(const char *str)
asciichat_error_t options_config_generate_final_manpage(const char *template_path, const char *output_path, const char *version_string, const char *content_file_path)
asciichat_error_t options_builder_generate_manpage_template(options_builder_t *builder, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
asciichat_error_t options_config_generate_manpage_template(const options_config_t *config, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
void free_parsed_sections(parsed_section_t *sections, size_t num_sections)
parsed_section_t * parse_manpage_sections(const char *filepath, size_t *num_sections)
asciichat_error_t options_config_generate_manpage_merged(const options_config_t *config, const char *program_name, const char *mode_name, const char *output_path, const char *brief_description)
asciichat_error_t manpage_parser_parse_file(FILE *f, parsed_section_t **out_sections, size_t *out_count)
Definition parser.c:339
const parsed_section_t * manpage_parser_find_section(const parsed_section_t *sections, size_t count, const char *section_name)
Definition parser.c:397
void manpage_parser_free_sections(parsed_section_t *sections, size_t count)
Definition parser.c:380
void manpage_content_free_positional(char *content)
Definition positional.c:64
char * manpage_content_generate_positional(const options_config_t *config)
Definition positional.c:15
void manpage_resources_destroy(manpage_resources_t *resources)
Definition resources.c:151
asciichat_error_t manpage_resources_load(manpage_resources_t *resources)
Definition resources.c:107
bool manpage_resources_is_valid(const manpage_resources_t *resources)
Definition resources.c:180
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
FILE * platform_fopen(const char *filename, const char *mode)