apex-ruby 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/ext/apex_ext/apex_ext.c +6 -0
  3. data/ext/apex_ext/apex_src/AGENTS.md +41 -0
  4. data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
  5. data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
  6. data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
  7. data/ext/apex_ext/apex_src/Package.swift +9 -0
  8. data/ext/apex_ext/apex_src/README.md +31 -9
  9. data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
  10. data/ext/apex_ext/apex_src/VERSION +1 -1
  11. data/ext/apex_ext/apex_src/cli/main.c +1125 -13
  12. data/ext/apex_ext/apex_src/docs/index.md +459 -0
  13. data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
  14. data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
  15. data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
  16. data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
  17. data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
  18. data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
  19. data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
  20. data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
  21. data/ext/apex_ext/apex_src/man/apex.1 +663 -620
  22. data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
  23. data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
  24. data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
  25. data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
  26. data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
  27. data/ext/apex_ext/apex_src/pages/index.md +459 -0
  28. data/ext/apex_ext/apex_src/src/_README.md +4 -4
  29. data/ext/apex_ext/apex_src/src/apex.c +702 -44
  30. data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
  31. data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
  32. data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
  33. data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
  34. data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
  35. data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
  36. data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
  37. data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
  38. data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
  39. data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
  40. data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
  41. data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
  42. data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
  43. data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
  44. data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
  45. data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
  46. data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
  47. data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
  48. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
  49. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
  50. data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
  51. data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
  52. data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
  53. data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
  54. data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
  55. data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
  56. data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
  57. data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
  58. data/ext/apex_ext/apex_src/tests/README.md +11 -5
  59. data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
  60. data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
  61. data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
  62. data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
  63. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
  64. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
  65. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
  66. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
  67. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
  68. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
  69. data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
  70. data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
  71. data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
  72. data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
  73. data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
  74. data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
  75. data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
  76. data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
  77. data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
  78. data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
  79. data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
  80. data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
  81. data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
  82. data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
  83. data/lib/apex/version.rb +1 -1
  84. metadata +32 -2
  85. data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +0 -456
@@ -0,0 +1,948 @@
1
+ /*
2
+ * Man page output: roff (man page source) and styled HTML.
3
+ * Renders cmark AST to .TH/.SH-style roff or a self-contained man-style HTML document.
4
+ */
5
+
6
+ #include "apex/ast_man.h"
7
+ #include "apex/parser.h"
8
+ #include "extensions/definition_list.h"
9
+ #include "extensions/syntax_highlight.h"
10
+ #include <stdbool.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <ctype.h>
14
+ #include <stdio.h>
15
+
16
+ /* ------------------------------------------------------------------------- */
17
+ /* Buffer and roff escape */
18
+ /* ------------------------------------------------------------------------- */
19
+
20
+ typedef struct {
21
+ char *buf;
22
+ size_t len;
23
+ size_t capacity;
24
+ } man_buffer;
25
+
26
+ static void man_buf_init(man_buffer *b) {
27
+ b->buf = NULL;
28
+ b->len = 0;
29
+ b->capacity = 0;
30
+ }
31
+
32
+ static void man_buf_append(man_buffer *b, const char *str, size_t len) {
33
+ if (!str || len == 0) return;
34
+ if (b->len + len + 1 > b->capacity) {
35
+ size_t new_cap = b->capacity ? b->capacity * 2 : 512;
36
+ if (new_cap < b->len + len + 1) new_cap = b->len + len + 1;
37
+ char *new_buf = (char *)realloc(b->buf, new_cap);
38
+ if (!new_buf) return;
39
+ b->buf = new_buf;
40
+ b->capacity = new_cap;
41
+ }
42
+ memcpy(b->buf + b->len, str, len);
43
+ b->len += len;
44
+ b->buf[b->len] = '\0';
45
+ }
46
+
47
+ static void man_buf_append_str(man_buffer *b, const char *str) {
48
+ if (str) man_buf_append(b, str, strlen(str));
49
+ }
50
+
51
+ /* Append text escaped for roff: \ -> \e, - -> \-, en-dash -> \-\-, leading . or ' -> \&. or \&' */
52
+ static void man_buf_append_roff_safe(man_buffer *b, const char *str, size_t len) {
53
+ if (!str || len == 0) return;
54
+ bool at_line_start = (b->len == 0 || (b->len > 0 && b->buf[b->len - 1] == '\n'));
55
+ for (size_t i = 0; i < len; ) {
56
+ unsigned char c = (unsigned char)str[i];
57
+ if (c == '\\') {
58
+ man_buf_append_str(b, "\\e");
59
+ i++;
60
+ at_line_start = false;
61
+ } else if (c == '\n') {
62
+ man_buf_append(b, "\n", 1);
63
+ i++;
64
+ at_line_start = true;
65
+ } else if (at_line_start && (c == '.' || c == '\'')) {
66
+ man_buf_append_str(b, "\\&");
67
+ man_buf_append(b, (const char *)&c, 1);
68
+ i++;
69
+ at_line_start = false;
70
+ } else if (c == 0x2D) {
71
+ /* hyphen-minus: use \- so man doesn't break line on it; keeps -- visible */
72
+ man_buf_append_str(b, "\\-");
73
+ i++;
74
+ at_line_start = false;
75
+ } else if (c == 0xE2 && i + 2 <= len && (unsigned char)str[i+1] == 0x80 && (unsigned char)str[i+2] == 0x93) {
76
+ /* UTF-8 en-dash (U+2013): often from smart typography -- ; show as two hyphens */
77
+ man_buf_append_str(b, "\\-\\-");
78
+ i += 3;
79
+ at_line_start = false;
80
+ } else if (c == 0xE2 && i + 2 <= len && (unsigned char)str[i+1] == 0x80 && (unsigned char)str[i+2] == 0x94) {
81
+ /* UTF-8 em-dash (U+2014) */
82
+ man_buf_append_str(b, "\\[em]");
83
+ i += 3;
84
+ at_line_start = false;
85
+ } else {
86
+ man_buf_append(b, (const char *)&c, 1);
87
+ i++;
88
+ at_line_start = false;
89
+ }
90
+ }
91
+ }
92
+
93
+ /* ------------------------------------------------------------------------- */
94
+ /* HTML <dl>/<dt>/<dd> from definition-list preprocessor -> roff */
95
+ /* ------------------------------------------------------------------------- */
96
+
97
+ /* Find next '>' from str+pos; return offset of '>' or len if not found. */
98
+ static size_t find_gt(const char *str, size_t len, size_t pos) {
99
+ for (; pos < len && str[pos] != '>'; pos++) {}
100
+ return pos;
101
+ }
102
+
103
+ /* Decode one entity at str (e.g. &lt; &gt; &amp;) and append to buf; return number of chars consumed. */
104
+ static size_t decode_entity(man_buffer *buf, const char *str, size_t len) {
105
+ if (len < 3 || str[0] != '&') return 0;
106
+ if (str[1] == 'l' && str[2] == 't' && len >= 4 && str[3] == ';') {
107
+ man_buf_append_str(buf, "<");
108
+ return 4;
109
+ }
110
+ if (str[1] == 'g' && str[2] == 't' && len >= 4 && str[3] == ';') {
111
+ man_buf_append_str(buf, ">");
112
+ return 4;
113
+ }
114
+ if (str[1] == 'a' && str[2] == 'm' && str[3] == 'p' && len >= 5 && str[4] == ';') {
115
+ man_buf_append_str(buf, "&");
116
+ return 5;
117
+ }
118
+ if (str[1] == 'q' && str[2] == 'u' && str[3] == 'o' && str[4] == 't' && len >= 6 && str[5] == ';') {
119
+ man_buf_append_str(buf, "\"");
120
+ return 6;
121
+ }
122
+ if (str[1] == '#' && len >= 4 && str[2] == '3' && str[3] == '9' && len >= 5 && str[4] == ';') {
123
+ man_buf_append_str(buf, "'");
124
+ return 5;
125
+ }
126
+ return 0;
127
+ }
128
+
129
+ /* Append HTML fragment (dt/dd content) as roff: handle <strong>, <em>, <code>, entities; strip other tags. */
130
+ static void append_html_fragment_roff(man_buffer *buf, const char *str, size_t len) {
131
+ size_t i = 0;
132
+ while (i < len) {
133
+ if (str[i] == '<') {
134
+ size_t end = find_gt(str, len, i);
135
+ if (end < len) {
136
+ /* tag from i to end (inclusive) */
137
+ size_t tag_len = end - i + 1;
138
+ if (tag_len == 8 && strncmp(str + i, "<strong>", 8) == 0)
139
+ man_buf_append_str(buf, "\\f[B]");
140
+ else if (tag_len == 9 && strncmp(str + i, "</strong>", 9) == 0)
141
+ man_buf_append_str(buf, "\\f[]");
142
+ else if (tag_len == 5 && strncmp(str + i, "<em>", 5) == 0)
143
+ man_buf_append_str(buf, "\\f[I]");
144
+ else if (tag_len == 6 && strncmp(str + i, "</em>", 6) == 0)
145
+ man_buf_append_str(buf, "\\f[]");
146
+ else if (tag_len == 6 && strncmp(str + i, "<code>", 6) == 0)
147
+ man_buf_append_str(buf, "\\fR");
148
+ else if (tag_len == 7 && strncmp(str + i, "</code>", 7) == 0)
149
+ man_buf_append_str(buf, "\\f[]");
150
+ i = end + 1;
151
+ continue;
152
+ }
153
+ }
154
+ if (str[i] == '&') {
155
+ size_t consumed = decode_entity(buf, str + i, len - i);
156
+ if (consumed > 0) {
157
+ i += consumed;
158
+ continue;
159
+ }
160
+ }
161
+ /* plain text run */
162
+ size_t start = i;
163
+ while (i < len && str[i] != '<' && str[i] != '&') i++;
164
+ if (i > start)
165
+ man_buf_append_roff_safe(buf, str + start, i - start);
166
+ }
167
+ }
168
+
169
+ /* Return true if str starts with <dl> (optional whitespace). */
170
+ static bool is_dl_block(const char *str, size_t len) {
171
+ while (len > 0 && (*str == ' ' || *str == '\n' || *str == '\t')) { str++; len--; }
172
+ return len >= 4 && str[0] == '<' && str[1] == 'd' && str[2] == 'l' && (str[3] == '>' || (len > 4 && str[3] == ' '));
173
+ }
174
+
175
+ /* Find content of first <dt>...</dt>: set *start and *content_len (inner text only). Return true if found. */
176
+ static bool find_dt(const char *str, size_t len, size_t *start, size_t *content_len) {
177
+ const char *p = str;
178
+ size_t rem = len;
179
+ while (rem >= 4 && (p[0] != '<' || p[1] != 'd' || p[2] != 't')) {
180
+ p++; rem--;
181
+ }
182
+ if (rem < 4) return false;
183
+ p += 3; rem -= 3; /* skip <dt */
184
+ while (rem > 0 && *p != '>') { p++; rem--; }
185
+ if (rem == 0) return false;
186
+ p++; rem--; /* skip '>' */
187
+ while (rem > 0 && (*p == ' ' || *p == '\n')) { p++; rem--; }
188
+ const char *inner_start = p;
189
+ while (rem >= 6) {
190
+ if (p[0] == '<' && p[1] == '/' && p[2] == 'd' && p[3] == 't' && p[4] == '>') {
191
+ *start = (size_t)(inner_start - str);
192
+ *content_len = (size_t)(p - inner_start);
193
+ return true;
194
+ }
195
+ p++; rem--;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ static bool find_dd(const char *str, size_t len, size_t *start, size_t *content_len) {
201
+ const char *p = str;
202
+ size_t rem = len;
203
+ while (rem >= 4 && (p[0] != '<' || p[1] != 'd' || p[2] != 'd')) {
204
+ p++; rem--;
205
+ }
206
+ if (rem < 4) return false;
207
+ p += 3; rem -= 3; /* skip <dd */
208
+ while (rem > 0 && *p != '>') { p++; rem--; }
209
+ if (rem == 0) return false;
210
+ p++; rem--; /* skip '>' */
211
+ while (rem > 0 && (*p == ' ' || *p == '\n')) { p++; rem--; }
212
+ const char *inner_start = p;
213
+ while (rem >= 6) {
214
+ if (p[0] == '<' && p[1] == '/' && p[2] == 'd' && p[3] == 'd' && p[4] == '>') {
215
+ *start = (size_t)(inner_start - str);
216
+ *content_len = (size_t)(p - inner_start);
217
+ return true;
218
+ }
219
+ p++; rem--;
220
+ }
221
+ return false;
222
+ }
223
+
224
+ /* If literal is a <dl><dt>...</dt><dd>...</dd></dl> block, emit roff and return true. */
225
+ static bool render_dl_html_block_as_roff(man_buffer *buf, const char *lit, size_t lit_len) {
226
+ if (!lit || !is_dl_block(lit, lit_len)) return false;
227
+ size_t dt_start, dt_len, dd_start, dd_len;
228
+ if (!find_dt(lit, lit_len, &dt_start, &dt_len)) return false;
229
+ if (!find_dd(lit, lit_len, &dd_start, &dd_len)) return false;
230
+ man_buf_append_str(buf, "\n.TP\n");
231
+ append_html_fragment_roff(buf, lit + dt_start, dt_len);
232
+ man_buf_append_str(buf, "\n");
233
+ append_html_fragment_roff(buf, lit + dd_start, dd_len);
234
+ man_buf_append_str(buf, "\n");
235
+ return true;
236
+ }
237
+
238
+ /* ------------------------------------------------------------------------- */
239
+ /* Helpers: get plain text from a node (for .TH title) */
240
+ /* ------------------------------------------------------------------------- */
241
+
242
+ static void collect_plain_text(cmark_node *node, man_buffer *out) {
243
+ if (!node) return;
244
+ cmark_node_type t = cmark_node_get_type(node);
245
+ if (t == CMARK_NODE_TEXT || t == CMARK_NODE_CODE) {
246
+ const char *lit = cmark_node_get_literal(node);
247
+ if (lit) man_buf_append(out, lit, strlen(lit));
248
+ return;
249
+ }
250
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur)) {
251
+ collect_plain_text(cur, out);
252
+ }
253
+ }
254
+
255
+ /* Caller frees. Returns first H1 heading text or NULL. */
256
+ static char *get_first_h1_text(cmark_node *document) {
257
+ if (!document || cmark_node_get_type(document) != CMARK_NODE_DOCUMENT) return NULL;
258
+ for (cmark_node *cur = cmark_node_first_child(document); cur; cur = cmark_node_next(cur)) {
259
+ if (cmark_node_get_type(cur) == CMARK_NODE_HEADING && cmark_node_get_heading_level(cur) == 1) {
260
+ man_buffer b;
261
+ man_buf_init(&b);
262
+ collect_plain_text(cur, &b);
263
+ if (b.len > 0 && b.buf) {
264
+ char *s = strdup(b.buf);
265
+ free(b.buf);
266
+ return s;
267
+ }
268
+ if (b.buf) free(b.buf);
269
+ return NULL;
270
+ }
271
+ }
272
+ return NULL;
273
+ }
274
+
275
+ /* Caller frees. Returns plain text of first paragraph after NAME heading, or NULL. */
276
+ static char *get_name_section_paragraph_text(cmark_node *document) {
277
+ if (!document || cmark_node_get_type(document) != CMARK_NODE_DOCUMENT) return NULL;
278
+ cmark_node *name_heading = NULL;
279
+ for (cmark_node *cur = cmark_node_first_child(document); cur; cur = cmark_node_next(cur)) {
280
+ if (cmark_node_get_type(cur) == CMARK_NODE_HEADING && cmark_node_get_heading_level(cur) == 1) {
281
+ man_buffer b;
282
+ man_buf_init(&b);
283
+ collect_plain_text(cur, &b);
284
+ if (b.len > 0 && b.buf) {
285
+ if (strcmp(b.buf, "NAME") == 0) {
286
+ name_heading = cur;
287
+ free(b.buf);
288
+ break;
289
+ }
290
+ free(b.buf);
291
+ }
292
+ }
293
+ }
294
+ if (!name_heading) return NULL;
295
+ for (cmark_node *cur = cmark_node_next(name_heading); cur; cur = cmark_node_next(cur)) {
296
+ if (cmark_node_get_type(cur) == CMARK_NODE_PARAGRAPH) {
297
+ man_buffer b;
298
+ man_buf_init(&b);
299
+ collect_plain_text(cur, &b);
300
+ if (b.len > 0 && b.buf) {
301
+ char *s = strdup(b.buf);
302
+ free(b.buf);
303
+ return s;
304
+ }
305
+ if (b.buf) free(b.buf);
306
+ return NULL;
307
+ }
308
+ if (cmark_node_get_type(cur) == CMARK_NODE_HEADING) break;
309
+ }
310
+ return NULL;
311
+ }
312
+
313
+ /* Normalize s: trim, collapse runs of whitespace (including newlines) to single space. Modifies s. */
314
+ static void normalize_whitespace(char *s) {
315
+ if (!s || !*s) return;
316
+ char *r = s, *w = s;
317
+ while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++;
318
+ while (*r) {
319
+ if (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') {
320
+ *w++ = ' ';
321
+ do r++; while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r');
322
+ } else {
323
+ *w++ = *r++;
324
+ }
325
+ }
326
+ while (w > s && (w[-1] == ' ' || w[-1] == '\t')) w--;
327
+ *w = '\0';
328
+ }
329
+
330
+ /* ------------------------------------------------------------------------- */
331
+ /* Inline roff rendering */
332
+ /* ------------------------------------------------------------------------- */
333
+
334
+ static void render_inline_roff(man_buffer *buf, cmark_node *node);
335
+
336
+ static void render_inline_roff(man_buffer *buf, cmark_node *node) {
337
+ if (!node) return;
338
+ cmark_node_type t = cmark_node_get_type(node);
339
+ switch (t) {
340
+ case CMARK_NODE_TEXT: {
341
+ const char *lit = cmark_node_get_literal(node);
342
+ if (lit) man_buf_append_roff_safe(buf, lit, strlen(lit));
343
+ break;
344
+ }
345
+ case CMARK_NODE_CODE: {
346
+ /* Use roman (\fR) for code; \f[C] causes "cannot select font 'C'" on some groff devices */
347
+ man_buf_append_str(buf, "\\fR");
348
+ const char *lit = cmark_node_get_literal(node);
349
+ if (lit) man_buf_append_roff_safe(buf, lit, strlen(lit));
350
+ man_buf_append_str(buf, "\\f[]");
351
+ break;
352
+ }
353
+ case CMARK_NODE_LINEBREAK:
354
+ man_buf_append_str(buf, "\n.br\n");
355
+ break;
356
+ case CMARK_NODE_SOFTBREAK:
357
+ man_buf_append_str(buf, " ");
358
+ break;
359
+ case CMARK_NODE_STRONG:
360
+ man_buf_append_str(buf, "\\f[B]");
361
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
362
+ render_inline_roff(buf, c);
363
+ man_buf_append_str(buf, "\\f[]");
364
+ break;
365
+ case CMARK_NODE_EMPH:
366
+ man_buf_append_str(buf, "\\f[I]");
367
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
368
+ render_inline_roff(buf, c);
369
+ man_buf_append_str(buf, "\\f[]");
370
+ break;
371
+ case CMARK_NODE_LINK: {
372
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
373
+ render_inline_roff(buf, c);
374
+ const char *url = cmark_node_get_url(node);
375
+ if (url && url[0]) {
376
+ man_buf_append_str(buf, " (");
377
+ man_buf_append_roff_safe(buf, url, strlen(url));
378
+ man_buf_append_str(buf, ")");
379
+ }
380
+ break;
381
+ }
382
+ case CMARK_NODE_HTML_INLINE:
383
+ /* skip */
384
+ break;
385
+ default:
386
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
387
+ render_inline_roff(buf, c);
388
+ break;
389
+ }
390
+ }
391
+
392
+ /* ------------------------------------------------------------------------- */
393
+ /* Block roff rendering */
394
+ /* ------------------------------------------------------------------------- */
395
+
396
+ static void render_block_roff(man_buffer *buf, cmark_node *node);
397
+
398
+ /* True after we emitted a <dl> block so the next paragraph is definition continuation (no .PP). */
399
+ static bool roff_last_was_dl_dd = false;
400
+
401
+ static void render_block_roff(man_buffer *buf, cmark_node *node) {
402
+ if (!node) return;
403
+ cmark_node_type t = cmark_node_get_type(node);
404
+ switch (t) {
405
+ case CMARK_NODE_DOCUMENT:
406
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
407
+ render_block_roff(buf, cur);
408
+ break;
409
+ case CMARK_NODE_HEADING: {
410
+ roff_last_was_dl_dd = false;
411
+ int level = cmark_node_get_heading_level(node);
412
+ man_buf_append_str(buf, level == 1 ? "\n.SH " : "\n.SS ");
413
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
414
+ render_inline_roff(buf, c);
415
+ man_buf_append_str(buf, "\n");
416
+ break;
417
+ }
418
+ case CMARK_NODE_PARAGRAPH: {
419
+ cmark_node *parent = cmark_node_parent(node);
420
+ cmark_node_type pt = parent ? cmark_node_get_type(parent) : (cmark_node_type)0;
421
+ bool in_item = (pt == CMARK_NODE_ITEM);
422
+ bool in_def_data_first =
423
+ (pt == (cmark_node_type)APEX_NODE_DEFINITION_DATA &&
424
+ !cmark_node_previous(node));
425
+ bool in_def_term = (pt == (cmark_node_type)APEX_NODE_DEFINITION_TERM);
426
+ bool continue_after_dd = roff_last_was_dl_dd;
427
+ bool para_has_content = (cmark_node_first_child(node) != NULL);
428
+ if (continue_after_dd) {
429
+ if (para_has_content)
430
+ roff_last_was_dl_dd = false;
431
+ /* else leave flag set so next block (e.g. code block) is treated as continuation */
432
+ }
433
+ if (!in_item && !in_def_data_first && !in_def_term && !continue_after_dd) {
434
+ man_buf_append_str(buf, "\n.PP\n");
435
+ }
436
+ /* After a dd continuation, join with a space so we don't get a stray line break */
437
+ if (continue_after_dd && para_has_content && buf->len > 0 && buf->buf[buf->len - 1] != '\n')
438
+ man_buf_append_str(buf, " ");
439
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
440
+ render_inline_roff(buf, c);
441
+ if (para_has_content)
442
+ man_buf_append_str(buf, "\n");
443
+ break;
444
+ }
445
+ case CMARK_NODE_LIST:
446
+ roff_last_was_dl_dd = false;
447
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
448
+ render_block_roff(buf, cur);
449
+ break;
450
+ case CMARK_NODE_ITEM: {
451
+ cmark_node *list = cmark_node_parent(node);
452
+ if (list && cmark_node_get_list_type(list) == CMARK_BULLET_LIST) {
453
+ man_buf_append_str(buf, "\n.IP \\(bu 2\n");
454
+ } else {
455
+ int idx = cmark_node_get_item_index(node);
456
+ char num[32];
457
+ snprintf(num, sizeof(num), "\n.IP \"%d.\" 4\n", idx);
458
+ man_buf_append_str(buf, num);
459
+ }
460
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
461
+ render_block_roff(buf, cur);
462
+ break;
463
+ }
464
+ case CMARK_NODE_CODE_BLOCK: {
465
+ const char *lit = cmark_node_get_literal(node);
466
+ cmark_node *parent = cmark_node_parent(node);
467
+ cmark_node_type pt = parent ? cmark_node_get_type(parent) : (cmark_node_type)0;
468
+ bool in_item = (pt == CMARK_NODE_ITEM);
469
+ /* Continuation: after <dl> dd, or inside list item (indented line in source) - no .PP/.nf/.fi */
470
+ if (roff_last_was_dl_dd || in_item) {
471
+ if (roff_last_was_dl_dd)
472
+ roff_last_was_dl_dd = false;
473
+ if (buf->len > 0 && buf->buf[buf->len - 1] != '\n')
474
+ man_buf_append_str(buf, " ");
475
+ if (lit) man_buf_append_roff_safe(buf, lit, strlen(lit));
476
+ man_buf_append_str(buf, "\n");
477
+ break;
478
+ }
479
+ roff_last_was_dl_dd = false;
480
+ /* \fR not \f[C] to avoid "cannot select font 'C'" on some groff devices */
481
+ man_buf_append_str(buf, "\n.PP\n.nf\n\\fR\n");
482
+ if (lit) {
483
+ /* Collapse runs of newlines to one so indented "lists" don't get extra blank lines */
484
+ size_t lit_len = strlen(lit);
485
+ for (size_t i = 0; i < lit_len; ) {
486
+ size_t run = 0;
487
+ while (i + run < lit_len && lit[i + run] == '\n') run++;
488
+ if (run > 0) {
489
+ man_buf_append_str(buf, "\n");
490
+ i += run;
491
+ } else {
492
+ size_t text = 0;
493
+ while (i + text < lit_len && lit[i + text] != '\n') text++;
494
+ if (text > 0) {
495
+ man_buf_append_roff_safe(buf, lit + i, text);
496
+ i += text;
497
+ } else {
498
+ i++;
499
+ }
500
+ }
501
+ }
502
+ }
503
+ man_buf_append_str(buf, "\n\\f[]\n.fi\n");
504
+ break;
505
+ }
506
+ case CMARK_NODE_BLOCK_QUOTE:
507
+ roff_last_was_dl_dd = false;
508
+ man_buf_append_str(buf, "\n.RS\n");
509
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
510
+ render_block_roff(buf, cur);
511
+ man_buf_append_str(buf, "\n.RE\n");
512
+ break;
513
+ case CMARK_NODE_THEMATIC_BREAK:
514
+ roff_last_was_dl_dd = false;
515
+ man_buf_append_str(buf, "\n.PP\n * * * * *\n");
516
+ break;
517
+ case CMARK_NODE_HTML_BLOCK: {
518
+ const char *lit = cmark_node_get_literal(node);
519
+ size_t lit_len = lit ? strlen(lit) : 0;
520
+ if (lit_len > 0 && render_dl_html_block_as_roff(buf, lit, lit_len))
521
+ roff_last_was_dl_dd = true;
522
+ break;
523
+ }
524
+ default:
525
+ roff_last_was_dl_dd = false;
526
+ /* Definition list (Apex extension): term = .TP + bold term, data = body */
527
+ if (t == (cmark_node_type)APEX_NODE_DEFINITION_LIST) {
528
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
529
+ render_block_roff(buf, cur);
530
+ break;
531
+ }
532
+ if (t == (cmark_node_type)APEX_NODE_DEFINITION_TERM) {
533
+ man_buf_append_str(buf, "\n.TP\n");
534
+ /* Term can contain a paragraph or direct inlines; recurse so paragraph content is emitted without .PP */
535
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
536
+ render_block_roff(buf, cur);
537
+ break;
538
+ }
539
+ if (t == (cmark_node_type)APEX_NODE_DEFINITION_DATA) {
540
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
541
+ render_block_roff(buf, cur);
542
+ break;
543
+ }
544
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
545
+ render_block_roff(buf, cur);
546
+ break;
547
+ }
548
+ }
549
+
550
+ /* ------------------------------------------------------------------------- */
551
+ /* Public API: roff */
552
+ /* ------------------------------------------------------------------------- */
553
+
554
+ char *apex_cmark_to_man_roff(cmark_node *document, const apex_options *options)
555
+ {
556
+ (void)options;
557
+ if (!document) return strdup(".TH stub 1 \"\" \"\"\n");
558
+
559
+ const char *title = "Document";
560
+ char *first_h1 = get_first_h1_text(document);
561
+ if (first_h1 && first_h1[0]) {
562
+ title = first_h1;
563
+ }
564
+
565
+ const char *section = "1";
566
+ const char *date = "1 January 1970";
567
+ const char *source = "";
568
+
569
+ man_buffer buf;
570
+ man_buf_init(&buf);
571
+ /* .TH title section date source - all args quoted if they contain spaces */
572
+ man_buf_append_str(&buf, ".TH \"");
573
+ man_buf_append_roff_safe(&buf, title, strlen(title));
574
+ man_buf_append_str(&buf, "\" \"");
575
+ man_buf_append_str(&buf, section);
576
+ man_buf_append_str(&buf, "\" \"");
577
+ man_buf_append_str(&buf, date);
578
+ man_buf_append_str(&buf, "\" \"");
579
+ man_buf_append_str(&buf, source);
580
+ man_buf_append_str(&buf, "\"\n");
581
+
582
+ render_block_roff(&buf, document);
583
+
584
+ if (first_h1) free(first_h1);
585
+
586
+ if (!buf.buf) return strdup(".TH stub 1 \"\" \"\"\n");
587
+ return buf.buf;
588
+ }
589
+
590
+ /* ------------------------------------------------------------------------- */
591
+ /* Man-HTML: styled HTML man page */
592
+ /* ------------------------------------------------------------------------- */
593
+
594
+ /* Append string with HTML entities escaped: & < > " '. Replace UTF-8 en-dash (U+2013)
595
+ * with "--" so option names like –standalone render as --standalone. */
596
+ static void man_buf_append_html_escaped(man_buffer *b, const char *str, size_t len) {
597
+ if (!str || len == 0) return;
598
+ for (size_t i = 0; i < len; i++) {
599
+ unsigned char c = (unsigned char)str[i];
600
+ if (i + 2 < len && c == 0xE2 && (unsigned char)str[i + 1] == 0x80 && (unsigned char)str[i + 2] == 0x93) {
601
+ man_buf_append_str(b, "--");
602
+ i += 2; /* skip the other two bytes of en-dash */
603
+ } else if (c == '&') man_buf_append_str(b, "&amp;");
604
+ else if (c == '<') man_buf_append_str(b, "&lt;");
605
+ else if (c == '>') man_buf_append_str(b, "&gt;");
606
+ else if (c == '"') man_buf_append_str(b, "&quot;");
607
+ else if (c == '\'') man_buf_append_str(b, "&#39;");
608
+ else man_buf_append(b, (const char *)&c, 1);
609
+ }
610
+ }
611
+
612
+ static const char *man_html_css =
613
+ "body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 65em; margin: 1em auto; padding: 0 1em; line-height: 1.4; color: #333; }\n"
614
+ "body.man-standalone { margin: 0; }\n"
615
+ ".man-headline { font-size: 1.75rem; font-weight: bold; margin: 0.5em 0 0.75em; border-bottom: none; color: #a02172; }\n"
616
+ ".man-nav { position: fixed; left: 0; top: 0; width: 14em; height: 100vh; overflow-y: auto; padding: 1.25em 1.5em; border-right: 1px solid #e0ddd6; background: #f5f4f0; font-size: 0.9rem; }\n"
617
+ ".man-nav ul { list-style: none; padding: 0; margin: 0; }\n"
618
+ ".man-nav li { margin: 0.35em 0; }\n"
619
+ ".man-nav a { color: #444; text-decoration: none; display: block; padding: 0.2em 0; }\n"
620
+ ".man-nav a:hover { color: #2376b1; background: rgba(0,0,0,0.03); }\n"
621
+ ".man-main { margin-left: 16em; padding: 1.5em 2em; max-width: 65em; }\n"
622
+ ".man-section { font-weight: bold; margin-top: 1em; margin-bottom: 0.25em; }\n"
623
+ ".man-section h2, .man-section h3, .man-section h4 { font-size: 1em; margin: 0; color: #3f789b; }\n"
624
+ "p { margin: 0.5em 0; }\n"
625
+ "strong, .man-option { color: #a02172; font-weight: bold; }\n"
626
+ "code, .man-option { font-family: monospace; background: #f5f5f5; padding: 0 0.2em; }\n"
627
+ "pre { background: #f5f5f5; padding: 0.75em; overflow-x: auto; }\n"
628
+ "ul, ol { margin: 0.5em 0; padding-left: 1.5em; }\n"
629
+ "a { color: #2376b1; }\n";
630
+
631
+ static void render_inline_man_html(man_buffer *buf, cmark_node *node);
632
+ static void render_block_man_html(man_buffer *buf, cmark_node *node);
633
+
634
+ static void render_inline_man_html(man_buffer *buf, cmark_node *node) {
635
+ if (!node) return;
636
+ cmark_node_type t = cmark_node_get_type(node);
637
+ switch (t) {
638
+ case CMARK_NODE_TEXT: {
639
+ const char *lit = cmark_node_get_literal(node);
640
+ if (lit) man_buf_append_html_escaped(buf, lit, strlen(lit));
641
+ break;
642
+ }
643
+ case CMARK_NODE_CODE:
644
+ man_buf_append_str(buf, "<code>");
645
+ if (cmark_node_get_literal(node))
646
+ man_buf_append_html_escaped(buf, cmark_node_get_literal(node), strlen(cmark_node_get_literal(node)));
647
+ man_buf_append_str(buf, "</code>");
648
+ break;
649
+ case CMARK_NODE_LINEBREAK:
650
+ man_buf_append_str(buf, "<br>\n");
651
+ break;
652
+ case CMARK_NODE_SOFTBREAK:
653
+ man_buf_append_str(buf, " ");
654
+ break;
655
+ case CMARK_NODE_STRONG:
656
+ man_buf_append_str(buf, "<strong>");
657
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
658
+ render_inline_man_html(buf, c);
659
+ man_buf_append_str(buf, "</strong>");
660
+ break;
661
+ case CMARK_NODE_EMPH:
662
+ man_buf_append_str(buf, "<em>");
663
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
664
+ render_inline_man_html(buf, c);
665
+ man_buf_append_str(buf, "</em>");
666
+ break;
667
+ case CMARK_NODE_LINK: {
668
+ const char *url = cmark_node_get_url(node);
669
+ if (url && url[0]) {
670
+ man_buf_append_str(buf, "<a href=\"");
671
+ man_buf_append_html_escaped(buf, url, strlen(url));
672
+ man_buf_append_str(buf, "\">");
673
+ }
674
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
675
+ render_inline_man_html(buf, c);
676
+ if (url && url[0]) man_buf_append_str(buf, "</a>");
677
+ break;
678
+ }
679
+ case CMARK_NODE_HTML_INLINE:
680
+ break;
681
+ default:
682
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
683
+ render_inline_man_html(buf, c);
684
+ break;
685
+ }
686
+ }
687
+
688
+ static void section_id_from_heading(cmark_node *node, man_buffer *buf) {
689
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c)) {
690
+ if (cmark_node_get_type(c) == CMARK_NODE_TEXT) {
691
+ const char *lit = cmark_node_get_literal(c);
692
+ if (lit) {
693
+ for (const char *p = lit; *p; p++) {
694
+ unsigned char ch = (unsigned char)*p;
695
+ if (ch == ' ' || ch == '\t') man_buf_append(buf, "-", 1);
696
+ else if (isalnum(ch) || ch == '-') man_buf_append(buf, (const char *)&ch, 1);
697
+ }
698
+ }
699
+ break;
700
+ }
701
+ section_id_from_heading(c, buf);
702
+ }
703
+ }
704
+
705
+ #define MAN_HTML_MAX_SECTIONS 48
706
+ typedef struct { char id[72]; char label[72]; } man_section_entry;
707
+
708
+ static size_t collect_man_sections(cmark_node *document, man_section_entry *out) {
709
+ size_t n = 0;
710
+ if (!document || cmark_node_get_type(document) != CMARK_NODE_DOCUMENT) return 0;
711
+ for (cmark_node *cur = cmark_node_first_child(document); cur && n < MAN_HTML_MAX_SECTIONS; cur = cmark_node_next(cur)) {
712
+ if (cmark_node_get_type(cur) != CMARK_NODE_HEADING || cmark_node_get_heading_level(cur) != 1) continue;
713
+ man_buffer id_buf, label_buf;
714
+ man_buf_init(&id_buf);
715
+ man_buf_init(&label_buf);
716
+ section_id_from_heading(cur, &id_buf);
717
+ collect_plain_text(cur, &label_buf);
718
+ if (id_buf.len > 0 && id_buf.buf) {
719
+ size_t id_len = id_buf.len < 71 ? id_buf.len : 71;
720
+ memcpy(out[n].id, id_buf.buf, id_len);
721
+ out[n].id[id_len] = '\0';
722
+ } else {
723
+ out[n].id[0] = '\0';
724
+ }
725
+ if (label_buf.len > 0 && label_buf.buf) {
726
+ size_t lab_len = label_buf.len < 71 ? label_buf.len : 71;
727
+ memcpy(out[n].label, label_buf.buf, lab_len);
728
+ out[n].label[lab_len] = '\0';
729
+ } else {
730
+ out[n].label[0] = '\0';
731
+ }
732
+ if (id_buf.buf) free(id_buf.buf);
733
+ if (label_buf.buf) free(label_buf.buf);
734
+ n++;
735
+ }
736
+ return n;
737
+ }
738
+
739
+ static void render_block_man_html(man_buffer *buf, cmark_node *node) {
740
+ if (!node) return;
741
+ cmark_node_type t = cmark_node_get_type(node);
742
+ switch (t) {
743
+ case CMARK_NODE_DOCUMENT:
744
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
745
+ render_block_man_html(buf, cur);
746
+ break;
747
+ case CMARK_NODE_HEADING: {
748
+ int level = cmark_node_get_heading_level(node);
749
+ int h = level + 1; /* h2 for level 1, h3 for level 2 */
750
+ if (h > 4) h = 4;
751
+ char tag[8];
752
+ snprintf(tag, sizeof(tag), "h%d", h);
753
+ man_buf_append_str(buf, "\n<div class=\"man-section\"><");
754
+ man_buf_append(buf, tag, strlen(tag));
755
+ man_buf_append_str(buf, " id=\"");
756
+ section_id_from_heading(node, buf);
757
+ man_buf_append_str(buf, "\">");
758
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
759
+ render_inline_man_html(buf, c);
760
+ man_buf_append_str(buf, "</");
761
+ man_buf_append(buf, tag, strlen(tag));
762
+ man_buf_append_str(buf, "></div>\n");
763
+ break;
764
+ }
765
+ case CMARK_NODE_PARAGRAPH: {
766
+ cmark_node *parent = cmark_node_parent(node);
767
+ bool in_item = (parent && cmark_node_get_type(parent) == CMARK_NODE_ITEM && !cmark_node_previous(node));
768
+ if (!in_item) man_buf_append_str(buf, "<p>");
769
+ for (cmark_node *c = cmark_node_first_child(node); c; c = cmark_node_next(c))
770
+ render_inline_man_html(buf, c);
771
+ if (!in_item) man_buf_append_str(buf, "</p>\n");
772
+ else man_buf_append_str(buf, "\n");
773
+ break;
774
+ }
775
+ case CMARK_NODE_LIST: {
776
+ cmark_list_type list_type = cmark_node_get_list_type(node);
777
+ man_buf_append_str(buf, list_type == CMARK_ORDERED_LIST ? "\n<ol>\n" : "\n<ul>\n");
778
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
779
+ render_block_man_html(buf, cur);
780
+ man_buf_append_str(buf, list_type == CMARK_ORDERED_LIST ? "</ol>\n" : "</ul>\n");
781
+ break;
782
+ }
783
+ case CMARK_NODE_ITEM:
784
+ man_buf_append_str(buf, "<li>");
785
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
786
+ render_block_man_html(buf, cur);
787
+ man_buf_append_str(buf, "</li>\n");
788
+ break;
789
+ case CMARK_NODE_CODE_BLOCK:
790
+ man_buf_append_str(buf, "\n<pre><code>");
791
+ if (cmark_node_get_literal(node))
792
+ man_buf_append_html_escaped(buf, cmark_node_get_literal(node), strlen(cmark_node_get_literal(node)));
793
+ man_buf_append_str(buf, "</code></pre>\n");
794
+ break;
795
+ case CMARK_NODE_BLOCK_QUOTE:
796
+ man_buf_append_str(buf, "\n<blockquote>\n");
797
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
798
+ render_block_man_html(buf, cur);
799
+ man_buf_append_str(buf, "</blockquote>\n");
800
+ break;
801
+ case CMARK_NODE_THEMATIC_BREAK:
802
+ man_buf_append_str(buf, "\n<hr>\n");
803
+ break;
804
+ case CMARK_NODE_HTML_BLOCK: {
805
+ const char *lit = cmark_node_get_literal(node);
806
+ size_t lit_len = lit ? strlen(lit) : 0;
807
+ if (lit_len > 0 && is_dl_block(lit, lit_len)) {
808
+ /* Append with en-dash (U+2013) replaced by -- so option names render correctly */
809
+ for (size_t i = 0; i < lit_len; i++) {
810
+ if (i + 2 < lit_len && (unsigned char)lit[i] == 0xE2
811
+ && (unsigned char)lit[i + 1] == 0x80 && (unsigned char)lit[i + 2] == 0x93) {
812
+ man_buf_append_str(buf, "--");
813
+ i += 2;
814
+ } else {
815
+ man_buf_append(buf, lit + i, 1);
816
+ }
817
+ }
818
+ }
819
+ break;
820
+ }
821
+ default:
822
+ for (cmark_node *cur = cmark_node_first_child(node); cur; cur = cmark_node_next(cur))
823
+ render_block_man_html(buf, cur);
824
+ break;
825
+ }
826
+ }
827
+
828
+ char *apex_cmark_to_man_html(cmark_node *document, const apex_options *options)
829
+ {
830
+ if (!document) return strdup("<!DOCTYPE html><html><body><p>stub</p></body></html>");
831
+
832
+ bool standalone = options && options->standalone;
833
+
834
+ if (!standalone) {
835
+ man_buffer buf;
836
+ man_buf_init(&buf);
837
+ render_block_man_html(&buf, document);
838
+ if (!buf.buf) return strdup("");
839
+ char *out = buf.buf;
840
+ if (options && options->code_highlighter && options->code_highlighter[0]) {
841
+ char *hl = apex_apply_syntax_highlighting(out,
842
+ options->code_highlighter,
843
+ false,
844
+ false,
845
+ false,
846
+ options->code_highlight_theme);
847
+ if (hl) {
848
+ free(out);
849
+ out = hl;
850
+ }
851
+ }
852
+ return out;
853
+ }
854
+
855
+ char *headline_cmd = NULL;
856
+ char *headline_desc = NULL;
857
+ char *name_line = get_name_section_paragraph_text(document);
858
+ if (name_line && name_line[0]) {
859
+ const char *sep = strstr(name_line, " - ");
860
+ if (sep) {
861
+ size_t cmd_len = (size_t)(sep - name_line);
862
+ headline_cmd = (char *)malloc(cmd_len + 1);
863
+ if (headline_cmd) {
864
+ memcpy(headline_cmd, name_line, cmd_len);
865
+ headline_cmd[cmd_len] = '\0';
866
+ normalize_whitespace(headline_cmd);
867
+ }
868
+ headline_desc = strdup(sep + 3);
869
+ if (headline_desc) normalize_whitespace(headline_desc);
870
+ } else {
871
+ headline_cmd = strdup(name_line);
872
+ if (headline_cmd) normalize_whitespace(headline_cmd);
873
+ headline_desc = strdup("manual page");
874
+ }
875
+ free(name_line);
876
+ }
877
+ if (!headline_cmd) headline_cmd = strdup("Document");
878
+ if (!headline_desc) headline_desc = strdup("manual page");
879
+ if (options->document_title && options->document_title[0]) {
880
+ const char *dt = options->document_title;
881
+ if (strchr(dt, '(') && strchr(dt, ')')) {
882
+ free(headline_cmd);
883
+ headline_cmd = strdup(dt);
884
+ }
885
+ }
886
+
887
+ man_section_entry sections[MAN_HTML_MAX_SECTIONS];
888
+ size_t n_sections = collect_man_sections(document, sections);
889
+
890
+ man_buffer buf;
891
+ man_buf_init(&buf);
892
+ man_buf_append_str(&buf, "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>");
893
+ man_buf_append_html_escaped(&buf, headline_cmd, strlen(headline_cmd));
894
+ man_buf_append_str(&buf, " — ");
895
+ man_buf_append_html_escaped(&buf, headline_desc, strlen(headline_desc));
896
+ man_buf_append_str(&buf, "</title>\n<style>\n");
897
+ man_buf_append_str(&buf, man_html_css);
898
+ man_buf_append_str(&buf, "</style>\n");
899
+ if (options->stylesheet_paths && options->stylesheet_count > 0) {
900
+ for (size_t i = 0; i < options->stylesheet_count && options->stylesheet_paths[i]; i++) {
901
+ man_buf_append_str(&buf, "<link rel=\"stylesheet\" href=\"");
902
+ man_buf_append_html_escaped(&buf, options->stylesheet_paths[i], strlen(options->stylesheet_paths[i]));
903
+ man_buf_append_str(&buf, "\">\n");
904
+ }
905
+ }
906
+ man_buf_append_str(&buf, "</head>\n<body class=\"man-standalone\">\n");
907
+
908
+ if (n_sections > 0) {
909
+ man_buf_append_str(&buf, "<nav class=\"man-nav\"><ul>\n");
910
+ for (size_t i = 0; i < n_sections; i++) {
911
+ if (sections[i].id[0]) {
912
+ man_buf_append_str(&buf, "<li><a href=\"#");
913
+ man_buf_append_html_escaped(&buf, sections[i].id, strlen(sections[i].id));
914
+ man_buf_append_str(&buf, "\">");
915
+ man_buf_append_html_escaped(&buf, sections[i].label, strlen(sections[i].label));
916
+ man_buf_append_str(&buf, "</a></li>\n");
917
+ }
918
+ }
919
+ man_buf_append_str(&buf, "</ul></nav>\n");
920
+ }
921
+
922
+ man_buf_append_str(&buf, "<main class=\"man-main\">\n<h1 class=\"man-headline\">");
923
+ man_buf_append_html_escaped(&buf, headline_cmd, strlen(headline_cmd));
924
+ man_buf_append_str(&buf, " — ");
925
+ man_buf_append_html_escaped(&buf, headline_desc, strlen(headline_desc));
926
+ man_buf_append_str(&buf, "</h1>\n");
927
+ if (headline_cmd) free(headline_cmd);
928
+ if (headline_desc) free(headline_desc);
929
+ render_block_man_html(&buf, document);
930
+ man_buf_append_str(&buf, "\n</main>\n</body>\n</html>");
931
+
932
+ if (!buf.buf) return strdup("<!DOCTYPE html><html><body><p>stub</p></body></html>");
933
+
934
+ char *out = buf.buf;
935
+ if (options->code_highlighter && options->code_highlighter[0]) {
936
+ char *hl = apex_apply_syntax_highlighting(out,
937
+ options->code_highlighter,
938
+ false,
939
+ false,
940
+ false,
941
+ options->code_highlight_theme);
942
+ if (hl) {
943
+ free(out);
944
+ out = hl;
945
+ }
946
+ }
947
+ return out;
948
+ }