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.
- checksums.yaml +4 -4
- data/ext/apex_ext/apex_ext.c +6 -0
- data/ext/apex_ext/apex_src/AGENTS.md +41 -0
- data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
- data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
- data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
- data/ext/apex_ext/apex_src/Package.swift +9 -0
- data/ext/apex_ext/apex_src/README.md +31 -9
- data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
- data/ext/apex_ext/apex_src/VERSION +1 -1
- data/ext/apex_ext/apex_src/cli/main.c +1125 -13
- data/ext/apex_ext/apex_src/docs/index.md +459 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
- data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
- data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
- data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
- data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
- data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
- data/ext/apex_ext/apex_src/man/apex.1 +663 -620
- data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
- data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
- data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
- data/ext/apex_ext/apex_src/pages/index.md +459 -0
- data/ext/apex_ext/apex_src/src/_README.md +4 -4
- data/ext/apex_ext/apex_src/src/apex.c +702 -44
- data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
- data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
- data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
- data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
- data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
- data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
- data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
- data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
- data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
- data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
- data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
- data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
- data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
- data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
- data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
- data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
- data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
- data/ext/apex_ext/apex_src/tests/README.md +11 -5
- data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
- data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
- data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
- data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
- data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
- data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
- data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
- data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
- data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
- data/lib/apex/version.rb +1 -1
- metadata +32 -2
- 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
|
|
180
|
-
* as normal data.
|
|
205
|
+
* The second row is emitted as normal data.
|
|
181
206
|
*/
|
|
182
|
-
char *
|
|
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, "\n", filepath);
|
|
1152
1349
|
} else if (file_type == FILE_TYPE_CSV || file_type == FILE_TYPE_TSV) {
|
|
1153
1350
|
/* CSV/TSV: convert to table */
|
|
1154
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 *
|
|
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
|
-
|
|
712
|
-
|
|
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--;
|