apex-ruby 1.0.6 → 1.0.8

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
@@ -48,6 +48,28 @@ static char *read_file_contents(const char *filepath) {
48
48
  return content;
49
49
  }
50
50
 
51
+ /**
52
+ * Decode percent-encoded path in place (RFC 3986: %XX -> byte).
53
+ * Used so include paths like with%20space.txt resolve to files with spaces.
54
+ */
55
+ static void percent_decode_inplace(char *path) {
56
+ if (!path) return;
57
+ char *r = path;
58
+ char *w = path;
59
+ while (*r) {
60
+ if (r[0] == '%' && r[1] && r[2] &&
61
+ isxdigit((unsigned char)r[1]) && isxdigit((unsigned char)r[2])) {
62
+ int hi = (r[1] <= '9') ? (r[1] - '0') : ((r[1] & 0x0f) + 9);
63
+ int lo = (r[2] <= '9') ? (r[2] - '0') : ((r[2] & 0x0f) + 9);
64
+ *w++ = (char)((hi << 4) | lo);
65
+ r += 3;
66
+ } else {
67
+ *w++ = *r++;
68
+ }
69
+ }
70
+ *w = '\0';
71
+ }
72
+
51
73
  /**
52
74
  * Resolve relative path from base directory
53
75
  */
@@ -174,15 +196,18 @@ static apex_file_type_t apex_detect_file_type(const char *filepath) {
174
196
  * - First row is always treated as header.
175
197
  * - If the second row cells are all one of: left, right, center, auto (case-insensitive),
176
198
  * it is treated as an alignment row and converted to :---, ---:, :---:, or ---.
199
+ * - Alternatively, if the second row cells contain only colons and dashes (e.g. :--, --:,
200
+ * :--:), they are parsed as Markdown-style alignment specs:
201
+ * :-- or :--- (colon at start) = left; --: or ---: (colon at end) = right;
202
+ * :--: or :---: (colon both ends) = center; --- (no colon) = auto.
177
203
  * The alignment row itself is NOT emitted as a data row.
178
204
  * - Otherwise, a default '---' separator row is generated after the header.
179
- * The second row (including rows that contain only '-' and ':' characters) is emitted
180
- * as normal data.
205
+ * The second row is emitted as normal data.
181
206
  */
182
- char *apex_csv_to_table(const char *csv_content, bool is_tsv) {
207
+ char *apex_csv_to_table_with_delimiter(const char *csv_content, bool is_tsv, char delimiter_override) {
183
208
  if (!csv_content) return NULL;
184
209
 
185
- char delim = is_tsv ? '\t' : ',';
210
+ char delim = delimiter_override ? delimiter_override : (is_tsv ? '\t' : ',');
186
211
  size_t len = strlen(csv_content);
187
212
  if (len == 0) return NULL;
188
213
 
@@ -368,6 +393,59 @@ char *apex_csv_to_table(const char *csv_content, bool is_tsv) {
368
393
  has_alignment_row = true;
369
394
  }
370
395
  }
396
+
397
+ /* If keywords failed, try Markdown-style alignment (:--, --:, :--:) */
398
+ if (!has_alignment_row && arow->cell_count == col_count) {
399
+ align = malloc((size_t)col_count * sizeof(*align));
400
+ if (align) {
401
+ bool all_colon_dash = true;
402
+ for (int i = 0; i < col_count && all_colon_dash; i++) {
403
+ char *cell = arow->cells[i];
404
+ char *start = cell;
405
+ while (*start && isspace((unsigned char)*start)) start++;
406
+ char *end = start + strlen(start);
407
+ while (end > start && isspace((unsigned char)end[-1])) end--;
408
+ size_t tlen = (size_t)(end - start);
409
+
410
+ if (tlen == 0) {
411
+ all_colon_dash = false;
412
+ break;
413
+ }
414
+
415
+ bool has_dash = false;
416
+ for (size_t j = 0; j < tlen; j++) {
417
+ char ch = start[j];
418
+ if (ch == '-') has_dash = true;
419
+ else if (ch != ':') {
420
+ all_colon_dash = false;
421
+ break;
422
+ }
423
+ }
424
+ if (!all_colon_dash || !has_dash) {
425
+ all_colon_dash = false;
426
+ break;
427
+ }
428
+
429
+ bool colon_start = (start[0] == ':');
430
+ bool colon_end = (end > start && end[-1] == ':');
431
+ if (colon_start && colon_end) {
432
+ align[i] = ALIGN_CENTER;
433
+ } else if (colon_start) {
434
+ align[i] = ALIGN_LEFT;
435
+ } else if (colon_end) {
436
+ align[i] = ALIGN_RIGHT;
437
+ } else {
438
+ align[i] = ALIGN_AUTO;
439
+ }
440
+ }
441
+ if (all_colon_dash) {
442
+ has_alignment_row = true;
443
+ } else {
444
+ free(align);
445
+ align = NULL;
446
+ }
447
+ }
448
+ }
371
449
  }
372
450
 
373
451
  /* Allocate output buffer: original size * 4 should be enough with extra alignment row */
@@ -452,6 +530,10 @@ char *apex_csv_to_table(const char *csv_content, bool is_tsv) {
452
530
  return output;
453
531
  }
454
532
 
533
+ char *apex_csv_to_table(const char *csv_content, bool is_tsv) {
534
+ return apex_csv_to_table_with_delimiter(csv_content, is_tsv, '\0');
535
+ }
536
+
455
537
  /**
456
538
  * Free address specification
457
539
  */
@@ -1069,6 +1151,118 @@ static char *normalize_fence_delimiters(const char *text) {
1069
1151
  return output;
1070
1152
  }
1071
1153
 
1154
+ /* Parse optional delimiter override tokens used after CSV/TSV includes.
1155
+ * Supported forms:
1156
+ * {;}
1157
+ * {delimiter=;}
1158
+ * {delimiter=\t}
1159
+ */
1160
+ static bool parse_include_delimiter_override(const char *start, const char **after_out, char *delimiter_out) {
1161
+ if (!start || !after_out || !delimiter_out) return false;
1162
+
1163
+ const char *p = start;
1164
+ while (*p == ' ' || *p == '\t') p++;
1165
+ if (*p != '{') return false;
1166
+
1167
+ const char *close = strchr(p + 1, '}');
1168
+ if (!close || close <= p + 1) return false;
1169
+
1170
+ char inner[64];
1171
+ size_t inner_len = (size_t)(close - (p + 1));
1172
+ if (inner_len >= sizeof(inner)) return false;
1173
+ memcpy(inner, p + 1, inner_len);
1174
+ inner[inner_len] = '\0';
1175
+
1176
+ char *s = inner;
1177
+ while (*s && isspace((unsigned char)*s)) s++;
1178
+ char *e = s + strlen(s);
1179
+ while (e > s && isspace((unsigned char)e[-1])) e--;
1180
+ *e = '\0';
1181
+
1182
+ if (s[0] == '\0') return false;
1183
+
1184
+ /* Shorthand: {;} */
1185
+ if (s[0] && s[1] == '\0') {
1186
+ *delimiter_out = s[0];
1187
+ *after_out = close + 1;
1188
+ return true;
1189
+ }
1190
+
1191
+ /* Verbose: {delimiter=;} */
1192
+ const char *key = "delimiter";
1193
+ size_t key_len = strlen(key);
1194
+ if (strncasecmp(s, key, key_len) != 0) return false;
1195
+ s += key_len;
1196
+ while (*s && isspace((unsigned char)*s)) s++;
1197
+ if (*s != '=') return false;
1198
+ s++;
1199
+ while (*s && isspace((unsigned char)*s)) s++;
1200
+
1201
+ if (s[0] == '\\' && s[1] == 't' && s[2] == '\0') {
1202
+ *delimiter_out = '\t';
1203
+ *after_out = close + 1;
1204
+ return true;
1205
+ }
1206
+ if (s[0] && s[1] == '\0') {
1207
+ *delimiter_out = s[0];
1208
+ *after_out = close + 1;
1209
+ return true;
1210
+ }
1211
+
1212
+ return false;
1213
+ }
1214
+
1215
+ /* Parse delimiter override embedded at end of filepath, e.g.:
1216
+ * data.csv{;}
1217
+ * data.csv{delimiter=;}
1218
+ * If found, trims filepath in-place and sets delimiter_out.
1219
+ */
1220
+ static bool parse_embedded_delimiter_override(char *filepath, char *delimiter_out) {
1221
+ if (!filepath || !delimiter_out) return false;
1222
+
1223
+ char *brace = strrchr(filepath, '{');
1224
+ if (!brace) return false;
1225
+ size_t tail_len = strlen(brace);
1226
+ if (tail_len < 3) return false; /* at least "{x}" */
1227
+ if (brace[tail_len - 1] != '}') return false;
1228
+
1229
+ const char *after = NULL;
1230
+ char parsed = '\0';
1231
+ if (!parse_include_delimiter_override(brace, &after, &parsed)) return false;
1232
+ if (after == NULL || *after != '\0') return false;
1233
+
1234
+ *brace = '\0';
1235
+ *delimiter_out = parsed;
1236
+ return true;
1237
+ }
1238
+
1239
+ /* Find closing "}}" for MMD transclusion, allowing embedded single-brace
1240
+ * segments inside filepath (e.g. {{data.csv{;}}}). */
1241
+ static const char *find_mmd_transclusion_end(const char *filepath_start) {
1242
+ if (!filepath_start) return NULL;
1243
+
1244
+ int brace_depth = 0;
1245
+ const char *p = filepath_start;
1246
+ while (*p) {
1247
+ if (p[0] == '{') {
1248
+ brace_depth++;
1249
+ p++;
1250
+ continue;
1251
+ }
1252
+ if (p[0] == '}' && brace_depth > 0) {
1253
+ brace_depth--;
1254
+ p++;
1255
+ continue;
1256
+ }
1257
+ if (p[0] == '}' && p[1] == '}' && brace_depth == 0) {
1258
+ return p;
1259
+ }
1260
+ p++;
1261
+ }
1262
+
1263
+ return NULL;
1264
+ }
1265
+
1072
1266
  /**
1073
1267
  * Process file includes in text
1074
1268
  */
@@ -1132,8 +1326,11 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1132
1326
  if (filepath_end > filepath_start && (filepath_end - filepath_start) < 1024) {
1133
1327
  char filepath[1024];
1134
1328
  size_t filepath_len = filepath_end - filepath_start;
1329
+ char ia_delimiter_override = '\0';
1135
1330
  memcpy(filepath, filepath_start, filepath_len);
1136
1331
  filepath[filepath_len] = '\0';
1332
+ percent_decode_inplace(filepath);
1333
+ parse_embedded_delimiter_override(filepath, &ia_delimiter_override);
1137
1334
 
1138
1335
  /* Resolve and check file exists */
1139
1336
  char *resolved_path = resolve_path(filepath, effective_base_dir);
@@ -1151,7 +1348,13 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1151
1348
  if (to_insert) snprintf(to_insert, buf_size, "![](%s)\n", filepath);
1152
1349
  } else if (file_type == FILE_TYPE_CSV || file_type == FILE_TYPE_TSV) {
1153
1350
  /* CSV/TSV: convert to table */
1154
- to_insert = apex_csv_to_table(content, file_type == FILE_TYPE_TSV);
1351
+ char delimiter_override = ia_delimiter_override;
1352
+ const char *override_end = filepath_end;
1353
+ if (delimiter_override == '\0' &&
1354
+ parse_include_delimiter_override(filepath_end, &override_end, &delimiter_override)) {
1355
+ filepath_end = override_end;
1356
+ }
1357
+ to_insert = apex_csv_to_table_with_delimiter(content, file_type == FILE_TYPE_TSV, delimiter_override);
1155
1358
  } else if (file_type == FILE_TYPE_CODE) {
1156
1359
  /* Code: wrap in fenced code block */
1157
1360
  const char *ext = strrchr(filepath, '.');
@@ -1212,14 +1415,17 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1212
1415
  /* Look for MMD transclusion {{file}} */
1213
1416
  if (!processed_include && !in_code_span && read_pos[0] == '{' && read_pos[1] == '{') {
1214
1417
  const char *filepath_start = read_pos + 2;
1215
- const char *filepath_end = strstr(filepath_start, "}}");
1418
+ const char *filepath_end = find_mmd_transclusion_end(filepath_start);
1216
1419
 
1217
1420
  if (filepath_end && (filepath_end - filepath_start) > 0 && (filepath_end - filepath_start) < 1024) {
1218
1421
  /* Extract filepath */
1219
1422
  int filepath_len = filepath_end - filepath_start;
1220
1423
  char filepath[1024];
1424
+ char mmd_delimiter_override = '\0';
1221
1425
  memcpy(filepath, filepath_start, filepath_len);
1222
1426
  filepath[filepath_len] = '\0';
1427
+ percent_decode_inplace(filepath);
1428
+ parse_embedded_delimiter_override(filepath, &mmd_delimiter_override);
1223
1429
 
1224
1430
  /* Check for address specification [address] */
1225
1431
  const char *address_start = filepath_end + 2;
@@ -1275,7 +1481,11 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1275
1481
 
1276
1482
  /* Convert CSV/TSV to table */
1277
1483
  if (file_type == FILE_TYPE_CSV || file_type == FILE_TYPE_TSV) {
1278
- char *table = apex_csv_to_table(extracted_content, file_type == FILE_TYPE_TSV);
1484
+ char *table = apex_csv_to_table_with_delimiter(
1485
+ extracted_content,
1486
+ file_type == FILE_TYPE_TSV,
1487
+ mmd_delimiter_override
1488
+ );
1279
1489
  if (table) {
1280
1490
  to_process = table;
1281
1491
  free_to_process = true;
@@ -1361,9 +1571,14 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1361
1571
  /* Extract filepath */
1362
1572
  int filepath_len = filepath_end - filepath_start;
1363
1573
  char filepath[1024];
1574
+ char marked_delimiter_override = '\0';
1364
1575
  if (filepath_len > 0 && filepath_len < (int)sizeof(filepath)) {
1365
1576
  memcpy(filepath, filepath_start, filepath_len);
1366
1577
  filepath[filepath_len] = '\0';
1578
+ percent_decode_inplace(filepath);
1579
+ if (bracket_type == '[') {
1580
+ parse_embedded_delimiter_override(filepath, &marked_delimiter_override);
1581
+ }
1367
1582
 
1368
1583
  /* Check for address specification [address] */
1369
1584
  const char *address_start = filepath_end + 1;
@@ -1418,7 +1633,11 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1418
1633
 
1419
1634
  /* Convert CSV/TSV to table */
1420
1635
  if (file_type == FILE_TYPE_CSV || file_type == FILE_TYPE_TSV) {
1421
- char *table = apex_csv_to_table(extracted_content, file_type == FILE_TYPE_TSV);
1636
+ char *table = apex_csv_to_table_with_delimiter(
1637
+ extracted_content,
1638
+ file_type == FILE_TYPE_TSV,
1639
+ marked_delimiter_override
1640
+ );
1422
1641
  if (table) {
1423
1642
  to_process = table;
1424
1643
  free_to_process = true;
@@ -1505,9 +1724,21 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
1505
1724
 
1506
1725
  /* Skip past the include syntax */
1507
1726
  if (address_end) {
1508
- read_pos = address_end + 1;
1727
+ const char *override_end = address_end + 1;
1728
+ char delimiter_unused = '\0';
1729
+ if (parse_include_delimiter_override(address_end + 1, &override_end, &delimiter_unused)) {
1730
+ read_pos = override_end;
1731
+ } else {
1732
+ read_pos = address_end + 1;
1733
+ }
1509
1734
  } else {
1510
- read_pos = filepath_end + 1;
1735
+ const char *override_end = filepath_end + 1;
1736
+ char delimiter_unused = '\0';
1737
+ if (parse_include_delimiter_override(filepath_end + 1, &override_end, &delimiter_unused)) {
1738
+ read_pos = override_end;
1739
+ } else {
1740
+ read_pos = filepath_end + 1;
1741
+ }
1511
1742
  }
1512
1743
  if (address_spec) free_address_spec(address_spec);
1513
1744
  processed_include = true;
@@ -39,6 +39,7 @@ char *apex_process_includes(const char *text, const char *base_dir, apex_metadat
39
39
  bool apex_file_exists(const char *filepath);
40
40
 
41
41
  char *apex_csv_to_table(const char *csv_content, bool is_tsv);
42
+ char *apex_csv_to_table_with_delimiter(const char *csv_content, bool is_tsv, char delimiter_override);
42
43
 
43
44
  /**
44
45
  * Resolve wildcard path (e.g., file.* -> file.html)
@@ -654,6 +654,116 @@ static apex_metadata_item *parse_mmd_metadata(const char *text, size_t *consumed
654
654
  return items;
655
655
  }
656
656
 
657
+ /* Check if a line consists only of at least min_count of delim_char (ignoring
658
+ * surrounding whitespace). */
659
+ static bool is_delimiter_line(const char *line, char delim_char, size_t min_count) {
660
+ if (!line) return false;
661
+ char temp[1024];
662
+ size_t len = strlen(line);
663
+ if (len >= sizeof(temp)) len = sizeof(temp) - 1;
664
+ memcpy(temp, line, len);
665
+ temp[len] = '\0';
666
+
667
+ char *trimmed = trim_whitespace(temp);
668
+ size_t tlen = strlen(trimmed);
669
+ if (tlen < min_count) return false;
670
+ for (size_t i = 0; i < tlen; i++) {
671
+ if (trimmed[i] != delim_char) return false;
672
+ }
673
+ return true;
674
+ }
675
+
676
+ /* Parse MultiMarkdown metadata when wrapped in delimiter lines.
677
+ * Opening delimiter: --- (or longer)
678
+ * Closing delimiter: --- (or longer) OR ... (or longer)
679
+ */
680
+ static apex_metadata_item *parse_mmd_metadata_delimited(const char *text, size_t *consumed) {
681
+ apex_metadata_item *items = NULL;
682
+ const char *line_start = text;
683
+ const char *line_end = strchr(line_start, '\n');
684
+ bool found_metadata = false;
685
+
686
+ if (!line_end) return NULL;
687
+
688
+ /* First line must be a dash delimiter */
689
+ {
690
+ size_t len = line_end - line_start;
691
+ char line[1024];
692
+ if (len >= sizeof(line)) len = sizeof(line) - 1;
693
+ memcpy(line, line_start, len);
694
+ line[len] = '\0';
695
+ if (!is_delimiter_line(line, '-', 3)) return NULL;
696
+ }
697
+
698
+ line_start = line_end + 1;
699
+ while ((line_end = strchr(line_start, '\n')) != NULL) {
700
+ size_t len = line_end - line_start;
701
+ char line[1024];
702
+ if (len >= sizeof(line)) len = sizeof(line) - 1;
703
+ memcpy(line, line_start, len);
704
+ line[len] = '\0';
705
+ char *trimmed = trim_whitespace(line);
706
+
707
+ /* End markers supported by MultiMarkdown compatibility */
708
+ if (is_delimiter_line(trimmed, '-', 3) || is_delimiter_line(trimmed, '.', 3)) {
709
+ if (found_metadata) {
710
+ *consumed = (line_end + 1) - text;
711
+ return items;
712
+ }
713
+ *consumed = 0;
714
+ return NULL;
715
+ }
716
+
717
+ /* Blank line also terminates metadata block */
718
+ if (*trimmed == '\0') {
719
+ if (found_metadata) {
720
+ *consumed = (line_end + 1) - text;
721
+ return items;
722
+ }
723
+ *consumed = 0;
724
+ return NULL;
725
+ }
726
+
727
+ char *colon = strchr(line, ':');
728
+ if (!colon) {
729
+ if (found_metadata) {
730
+ *consumed = line_start - text;
731
+ return items;
732
+ }
733
+ *consumed = 0;
734
+ return NULL;
735
+ }
736
+
737
+ if (colon[1] != ' ' && colon[1] != '\t') {
738
+ if (found_metadata) {
739
+ *consumed = line_start - text;
740
+ return items;
741
+ }
742
+ *consumed = 0;
743
+ return NULL;
744
+ }
745
+
746
+ *colon = '\0';
747
+ char *key = trim_whitespace(line);
748
+ char *value = trim_whitespace(colon + 1);
749
+ if (*key && *value) {
750
+ add_metadata_item(&items, key, value);
751
+ found_metadata = true;
752
+ } else {
753
+ if (found_metadata) {
754
+ *consumed = line_start - text;
755
+ return items;
756
+ }
757
+ *consumed = 0;
758
+ return NULL;
759
+ }
760
+
761
+ line_start = line_end + 1;
762
+ }
763
+
764
+ return NULL;
765
+ }
766
+
657
767
  /**
658
768
  * Parse Pandoc title block metadata
659
769
  * Format: % Title, % Author, % Date as first three lines
@@ -701,15 +811,27 @@ static apex_metadata_item *parse_pandoc_metadata(const char *text, size_t *consu
701
811
  * This modifies the input by removing the metadata section
702
812
  * Returns the extracted metadata
703
813
  */
704
- apex_metadata_item *apex_extract_metadata(char **text_ptr) {
814
+ apex_metadata_item *apex_extract_metadata_for_mode(char **text_ptr, apex_mode_t mode) {
705
815
  if (!text_ptr || !*text_ptr || !**text_ptr) return NULL;
706
816
 
707
817
  char *text = *text_ptr;
708
818
  size_t consumed = 0;
709
819
  apex_metadata_item *items = NULL;
710
820
 
711
- /* Try YAML first (most explicit) */
712
- if (strncmp(text, "---", 3) == 0) {
821
+ if (mode == APEX_MODE_MULTIMARKDOWN && strncmp(text, "---", 3) == 0) {
822
+ /* In MMD mode, delimiter blocks are treated as MultiMarkdown metadata,
823
+ * including dot-delimited closers (.../......). */
824
+ items = parse_mmd_metadata_delimited(text, &consumed);
825
+ if (!items) {
826
+ /* Keep YAML compatibility in MMD mode when content is actual YAML. */
827
+ items = parse_yaml_metadata(text, &consumed);
828
+ }
829
+ if (!items) {
830
+ items = parse_mmd_metadata(text, &consumed);
831
+ }
832
+ }
833
+ /* Try YAML first (most explicit) for non-MMD modes */
834
+ else if (strncmp(text, "---", 3) == 0) {
713
835
  items = parse_yaml_metadata(text, &consumed);
714
836
  }
715
837
  /* Try Pandoc */
@@ -730,6 +852,10 @@ apex_metadata_item *apex_extract_metadata(char **text_ptr) {
730
852
  return items;
731
853
  }
732
854
 
855
+ apex_metadata_item *apex_extract_metadata(char **text_ptr) {
856
+ return apex_extract_metadata_for_mode(text_ptr, APEX_MODE_UNIFIED);
857
+ }
858
+
733
859
  /**
734
860
  * Placeholder extension creation - for future full integration
735
861
  * For now, metadata is handled via preprocessing
@@ -2580,6 +2706,12 @@ void apex_apply_metadata_to_options(apex_metadata_item *metadata, apex_options *
2580
2706
  } else if (is_false_value(value)) {
2581
2707
  options->enable_footnotes = false;
2582
2708
  }
2709
+ } else if (strcasecmp(key, "one-line-definitions") == 0 || strcasecmp(key, "one_line_definitions") == 0) {
2710
+ if (is_true_value(value)) {
2711
+ options->enable_definition_lists = true;
2712
+ } else if (is_false_value(value)) {
2713
+ options->enable_definition_lists = false;
2714
+ }
2583
2715
  } else if (strcasecmp(key, "smart") == 0 || strcasecmp(key, "smart-typography") == 0) {
2584
2716
  if (is_true_value(value)) {
2585
2717
  options->enable_smart_typography = true;
@@ -2803,6 +2935,32 @@ void apex_apply_metadata_to_options(apex_metadata_item *metadata, apex_options *
2803
2935
  } else if (strcasecmp(key, "wikilink-sanitize") == 0 || strcasecmp(key, "wikilink_sanitize") == 0) {
2804
2936
  options->wikilink_sanitize = (strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0);
2805
2937
  }
2938
+ /* Terminal-specific options from config/metadata.
2939
+ * Keys are typically flattened from YAML mappings, e.g.:
2940
+ * terminal.theme (from terminal: { theme: brett })
2941
+ * terminal.width (from terminal: { width: 80 })
2942
+ */
2943
+ else if (strcasecmp(key, "terminal.theme") == 0 ||
2944
+ strcasecmp(key, "terminal_theme") == 0) {
2945
+ /* Only set theme_name if it wasn't already set by CLI or previous metadata. */
2946
+ if (!options->theme_name || !options->theme_name[0]) {
2947
+ options->theme_name = value;
2948
+ }
2949
+ } else if (strcasecmp(key, "terminal.width") == 0 ||
2950
+ strcasecmp(key, "terminal_width") == 0) {
2951
+ int w = atoi(value);
2952
+ if (w > 0) {
2953
+ options->terminal_width = w;
2954
+ }
2955
+ } else if (strcasecmp(key, "paginate") == 0 ||
2956
+ strcasecmp(key, "terminal.paginate") == 0 ||
2957
+ strcasecmp(key, "terminal_paginate") == 0) {
2958
+ if (is_true_value(value)) {
2959
+ options->paginate = true;
2960
+ } else if (is_false_value(value)) {
2961
+ options->paginate = false;
2962
+ }
2963
+ }
2806
2964
  /* Syntax highlighting options */
2807
2965
  else if (strcasecmp(key, "code-highlight") == 0 || strcasecmp(key, "code_highlight") == 0) {
2808
2966
  /* Accept full names and abbreviations */
@@ -2815,6 +2973,8 @@ void apex_apply_metadata_to_options(apex_metadata_item *metadata, apex_options *
2815
2973
  options->code_highlighter = "pygments";
2816
2974
  } else if (strcmp(lower, "skylighting") == 0 || strcmp(lower, "s") == 0 || strcmp(lower, "sky") == 0) {
2817
2975
  options->code_highlighter = "skylighting";
2976
+ } else if (strcmp(lower, "shiki") == 0 || strcmp(lower, "sh") == 0) {
2977
+ options->code_highlighter = "shiki";
2818
2978
  } else if (is_false_value(lower) || strcmp(lower, "none") == 0) {
2819
2979
  options->code_highlighter = NULL;
2820
2980
  }
@@ -2832,6 +2992,9 @@ void apex_apply_metadata_to_options(apex_metadata_item *metadata, apex_options *
2832
2992
  } else if (is_false_value(value)) {
2833
2993
  options->highlight_language_only = false;
2834
2994
  }
2995
+ } else if (strcasecmp(key, "code-highlight-theme") == 0 || strcasecmp(key, "code_highlight_theme") == 0) {
2996
+ /* Theme name for external highlighters (tool-specific). */
2997
+ options->code_highlight_theme = value;
2835
2998
  }
2836
2999
 
2837
3000
  item = item->next;
@@ -47,6 +47,13 @@ cmark_syntax_extension *create_metadata_extension(void);
47
47
  */
48
48
  apex_metadata_item *apex_extract_metadata(char **text_ptr);
49
49
 
50
+ /**
51
+ * Extract metadata from the beginning of text using mode-aware precedence.
52
+ * In MultiMarkdown mode, MMD metadata parsing takes precedence over YAML-style
53
+ * block parsing when an MMD delimiter block is detected.
54
+ */
55
+ apex_metadata_item *apex_extract_metadata_for_mode(char **text_ptr, apex_mode_t mode);
56
+
50
57
  /**
51
58
  * Get metadata from a document node
52
59
  * Returns a linked list of key-value pairs
@@ -10,6 +10,31 @@
10
10
  #include <stdbool.h>
11
11
  #include <ctype.h>
12
12
 
13
+ /** True if content at p looks like a list marker (- , * , + , or digit+. ) */
14
+ static bool looks_like_list_marker(const char *p) {
15
+ if (*p == '-' || *p == '*' || *p == '+')
16
+ return (p[1] == ' ' || p[1] == '\t');
17
+ if (isdigit((unsigned char)*p)) {
18
+ while (isdigit((unsigned char)*p)) p++;
19
+ return (*p == '.' && (p[1] == ' ' || p[1] == '\t'));
20
+ }
21
+ return false;
22
+ }
23
+
24
+ /** True if we're at the start of a line that is an indented code block (4+ spaces or tab)
25
+ * and not a list line. List lines (nested or continuation) should still get sup/sub. */
26
+ static bool line_is_indented_code_block(const char *read) {
27
+ if (*read == '\t') {
28
+ return !looks_like_list_marker(read + 1);
29
+ }
30
+ if (read[0] != ' ' || read[1] != ' ' || read[2] != ' ' || read[3] != ' ')
31
+ return false;
32
+ const char *content = read + 4;
33
+ while (*content == ' ')
34
+ content++;
35
+ return !looks_like_list_marker(content);
36
+ }
37
+
13
38
  /**
14
39
  * Process superscript and subscript syntax as preprocessing
15
40
  * Converts to <sup>text</sup> and <sub>text</sub> before parsing
@@ -31,11 +56,17 @@ char *apex_process_sup_sub(const char *text) {
31
56
 
32
57
  bool in_code_block = false;
33
58
  bool in_inline_code = false;
59
+ bool in_indented_code_block = false;
34
60
  bool in_math_inline = false;
35
61
  bool in_math_display = false;
36
62
  bool in_liquid = false;
37
63
 
38
64
  while (*read) {
65
+ /* At line start: indented code block only if 4+ spaces/tab and not a list line */
66
+ if (read == text || read[-1] == '\n') {
67
+ in_indented_code_block = line_is_indented_code_block(read);
68
+ }
69
+
39
70
  /* Track Liquid tags (skip processing inside them) */
40
71
  if (!in_liquid && *read == '{' && read[1] == '%') {
41
72
  in_liquid = true;
@@ -66,7 +97,7 @@ char *apex_process_sup_sub(const char *text) {
66
97
  }
67
98
  continue;
68
99
  }
69
- /* Track code blocks (skip processing inside them) */
100
+ /* Track fenced code blocks (skip processing inside them) */
70
101
  if (*read == '`') {
71
102
  if (read[1] == '`' && read[2] == '`') {
72
103
  in_code_block = !in_code_block;
@@ -77,7 +108,7 @@ char *apex_process_sup_sub(const char *text) {
77
108
 
78
109
  /* Track math spans (skip processing inside them) */
79
110
  bool handled_math = false;
80
- if (!in_code_block && !in_inline_code) {
111
+ if (!in_code_block && !in_inline_code && !in_indented_code_block) {
81
112
  /* Check for display math: $$...$$ */
82
113
  if (*read == '$' && read[1] == '$') {
83
114
  in_math_display = !in_math_display;
@@ -123,7 +154,7 @@ char *apex_process_sup_sub(const char *text) {
123
154
  }
124
155
 
125
156
  /* Skip processing inside code or math */
126
- if (handled_math || in_code_block || in_inline_code || in_math_inline || in_math_display) {
157
+ if (handled_math || in_code_block || in_inline_code || in_indented_code_block || in_math_inline || in_math_display) {
127
158
  if (!handled_math && remaining > 0) {
128
159
  *write++ = *read++;
129
160
  remaining--;