apex-ruby 1.0.7 → 1.0.9
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_src/CHANGELOG.md +69 -0
- data/ext/apex_ext/apex_src/CMakeLists.txt +2 -1
- data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
- data/ext/apex_ext/apex_src/Package.swift +14 -2
- data/ext/apex_ext/apex_src/README.md +12 -9
- data/ext/apex_ext/apex_src/VERSION +1 -1
- data/ext/apex_ext/apex_src/cli/main.c +625 -98
- data/ext/apex_ext/apex_src/ial.html +24 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +57 -7
- data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +3 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +8 -0
- data/ext/apex_ext/apex_src/include/apexc.h +6 -0
- data/ext/apex_ext/apex_src/include/module.modulemap +4 -0
- data/ext/apex_ext/apex_src/man/apex-config.5 +8 -2
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +13 -13
- data/ext/apex_ext/apex_src/man/apex.1 +150 -442
- data/ext/apex_ext/apex_src/man/apex.1.md +13 -0
- data/ext/apex_ext/apex_src/src/_README.md +3 -1
- data/ext/apex_ext/apex_src/src/apex.c +151 -6
- data/ext/apex_ext/apex_src/src/ast_terminal.c +459 -8
- data/ext/apex_ext/apex_src/src/extensions/advanced_tables.c +6 -6
- data/ext/apex_ext/apex_src/src/extensions/callouts.c +1 -1
- data/ext/apex_ext/apex_src/src/extensions/citations.c +24 -12
- data/ext/apex_ext/apex_src/src/extensions/critic.c +14 -6
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +2 -2
- data/ext/apex_ext/apex_src/src/extensions/grid_tables.c +1 -1
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +19 -6
- data/ext/apex_ext/apex_src/src/extensions/ial.c +25 -13
- data/ext/apex_ext/apex_src/src/extensions/includes.c +7 -7
- data/ext/apex_ext/apex_src/src/extensions/index.c +19 -7
- data/ext/apex_ext/apex_src/src/extensions/inline_footnotes.c +2 -2
- data/ext/apex_ext/apex_src/src/extensions/insert.c +1 -1
- data/ext/apex_ext/apex_src/src/extensions/math.c +11 -2
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +46 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +12 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +2 -2
- data/ext/apex_ext/apex_src/src/plugins.c +97 -55
- data/ext/apex_ext/apex_src/src/plugins.h +0 -10
- data/ext/apex_ext/apex_src/src/pretty_html.c +1 -1
- data/ext/apex_ext/apex_src/tests/fixtures/metadata/mmd-metadata.md +5 -0
- data/ext/apex_ext/apex_src/tests/fixtures/metadata/pandoc-meta.md +4 -0
- data/ext/apex_ext/apex_src/tests/fixtures/metadata/yaml-frontmatter.md +6 -0
- data/ext/apex_ext/apex_src/tests/metadata_cli_test.sh +119 -0
- data/ext/apex_ext/apex_src/tests/test_custom_plugins.c +78 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +27 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +42 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +83 -0
- data/ext/apex_ext/apex_src/tests/test_runner.c +4 -1
- data/lib/apex/version.rb +1 -1
- metadata +10 -2
|
@@ -92,7 +92,8 @@ static char *apex_git_toplevel(void) {
|
|
|
92
92
|
return out;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
|
|
96
|
+
typedef struct apex_plugin {
|
|
96
97
|
char *id;
|
|
97
98
|
char *title;
|
|
98
99
|
char *author;
|
|
@@ -101,7 +102,8 @@ struct apex_plugin {
|
|
|
101
102
|
char *repo;
|
|
102
103
|
apex_plugin_phase_mask phases;
|
|
103
104
|
int priority;
|
|
104
|
-
|
|
105
|
+
|
|
106
|
+
char *handler_command; /* External command to be executed */
|
|
105
107
|
int timeout_ms;
|
|
106
108
|
/* Declarative regex support */
|
|
107
109
|
char *pattern;
|
|
@@ -112,14 +114,20 @@ struct apex_plugin {
|
|
|
112
114
|
char *dir_path;
|
|
113
115
|
/* Per-plugin support directory (used for APEX_SUPPORT_DIR) */
|
|
114
116
|
char *support_dir;
|
|
117
|
+
/* A custom callback to transform the passed text. Executed only if handler_command and has_regex are not set. */
|
|
118
|
+
char *(*callback)(const char *text, const char *id_plugin, apex_plugin_phase_mask phase, const struct apex_options *options);
|
|
115
119
|
struct apex_plugin *next;
|
|
116
|
-
};
|
|
120
|
+
} apex_plugin;
|
|
117
121
|
|
|
118
122
|
struct apex_plugin_manager {
|
|
119
123
|
struct apex_plugin *pre_parse;
|
|
120
124
|
struct apex_plugin *post_render;
|
|
121
125
|
};
|
|
122
126
|
|
|
127
|
+
static apex_plugin *init_plugin(void) {
|
|
128
|
+
return calloc(1, sizeof(struct apex_plugin));
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
static void free_plugin(struct apex_plugin *p) {
|
|
124
132
|
while (p) {
|
|
125
133
|
struct apex_plugin *next = p->next;
|
|
@@ -137,6 +145,7 @@ static void free_plugin(struct apex_plugin *p) {
|
|
|
137
145
|
if (p->has_regex) {
|
|
138
146
|
regfree(&p->regex);
|
|
139
147
|
}
|
|
148
|
+
p->callback = NULL;
|
|
140
149
|
free(p);
|
|
141
150
|
p = next;
|
|
142
151
|
}
|
|
@@ -211,6 +220,31 @@ static bool plugin_id_exists(struct apex_plugin *head, const char *id) {
|
|
|
211
220
|
return false;
|
|
212
221
|
}
|
|
213
222
|
|
|
223
|
+
bool apex_plugin_register(apex_plugin_manager *manager, const char *id, apex_plugin_phase_mask phase, char *(*callback)(const char *text, const char *id_plugin, apex_plugin_phase_mask phase, const struct apex_options *options)) {
|
|
224
|
+
apex_plugin *plugin;
|
|
225
|
+
plugin = init_plugin();
|
|
226
|
+
if (!plugin || !manager) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
plugin->id = id != NULL ? strdup(id) : NULL;
|
|
231
|
+
plugin->callback = callback;
|
|
232
|
+
plugin->phases = phase;
|
|
233
|
+
|
|
234
|
+
if (plugin->phases & APEX_PLUGIN_PHASE_PRE_PARSE) {
|
|
235
|
+
if (!plugin_id_exists(manager->pre_parse, plugin->id)) {
|
|
236
|
+
append_plugin_sorted(&manager->pre_parse, plugin);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
} else if (plugin->phases & APEX_PLUGIN_PHASE_POST_RENDER) {
|
|
240
|
+
if (!plugin_id_exists(manager->post_render, plugin->id)) {
|
|
241
|
+
append_plugin_sorted(&manager->post_render, plugin);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
214
248
|
/* Determine base support directory for plugins, creating it if needed.
|
|
215
249
|
* This follows XDG conventions: $XDG_CONFIG_HOME/apex/support or
|
|
216
250
|
* $HOME/.config/apex/support.
|
|
@@ -355,7 +389,7 @@ static void load_plugins_from_dir(apex_plugin_manager *manager,
|
|
|
355
389
|
continue;
|
|
356
390
|
}
|
|
357
391
|
|
|
358
|
-
struct apex_plugin *p =
|
|
392
|
+
struct apex_plugin *p = init_plugin();
|
|
359
393
|
if (!p) {
|
|
360
394
|
apex_free_metadata(merged);
|
|
361
395
|
continue;
|
|
@@ -478,7 +512,7 @@ static void load_plugins_from_dir(apex_plugin_manager *manager,
|
|
|
478
512
|
}
|
|
479
513
|
|
|
480
514
|
const char *final_id = id ? id : ent->d_name;
|
|
481
|
-
struct apex_plugin *p =
|
|
515
|
+
struct apex_plugin *p = init_plugin();
|
|
482
516
|
if (!p) {
|
|
483
517
|
apex_free_metadata(meta);
|
|
484
518
|
continue;
|
|
@@ -568,66 +602,72 @@ apex_plugin_manager *apex_plugins_load(const apex_options *options) {
|
|
|
568
602
|
apex_plugin_manager *manager = calloc(1, sizeof(apex_plugin_manager));
|
|
569
603
|
if (!manager) return NULL;
|
|
570
604
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
cwd[0] = '\0';
|
|
574
|
-
if (getcwd(cwd, sizeof(cwd)) != NULL && cwd[0] != '\0') {
|
|
575
|
-
char *cwd_proj_dir = dup_join(cwd, ".apex/plugins");
|
|
576
|
-
if (cwd_proj_dir) {
|
|
577
|
-
load_plugins_from_dir(manager, cwd_proj_dir);
|
|
578
|
-
free(cwd_proj_dir);
|
|
579
|
-
}
|
|
605
|
+
if (options->plugin_register) {
|
|
606
|
+
options->plugin_register(manager, options);
|
|
580
607
|
}
|
|
581
608
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
char
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
609
|
+
if (options->allow_external_plugin_detection) {
|
|
610
|
+
/* Project-scoped (current working directory): CWD/.apex/plugins */
|
|
611
|
+
char cwd[1024];
|
|
612
|
+
cwd[0] = '\0';
|
|
613
|
+
if (getcwd(cwd, sizeof(cwd)) != NULL && cwd[0] != '\0') {
|
|
614
|
+
char *cwd_proj_dir = dup_join(cwd, ".apex/plugins");
|
|
615
|
+
if (cwd_proj_dir) {
|
|
616
|
+
load_plugins_from_dir(manager, cwd_proj_dir);
|
|
617
|
+
free(cwd_proj_dir);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* Project-scoped (explicit base_directory): base_directory/.apex/plugins */
|
|
622
|
+
if (options->base_directory && options->base_directory[0] != '\0') {
|
|
623
|
+
char *proj_dir = dup_join(options->base_directory, ".apex/plugins");
|
|
624
|
+
if (proj_dir) {
|
|
625
|
+
load_plugins_from_dir(manager, proj_dir);
|
|
626
|
+
free(proj_dir);
|
|
627
|
+
}
|
|
588
628
|
}
|
|
589
|
-
}
|
|
590
629
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
630
|
+
/* Project-scoped (Git repository root): <git top>/\.apex/plugins
|
|
631
|
+
* Only used when the current directory is inside the work tree.
|
|
632
|
+
*/
|
|
633
|
+
char *git_root = apex_git_toplevel();
|
|
634
|
+
if (git_root && git_root[0] != '\0' && cwd[0] != '\0') {
|
|
635
|
+
size_t root_len = strlen(git_root);
|
|
636
|
+
/* Ensure the Git root is a parent of (or equal to) the current directory. */
|
|
637
|
+
if (strncmp(cwd, git_root, root_len) == 0 &&
|
|
638
|
+
(cwd[root_len] == '/' || cwd[root_len] == '\0')) {
|
|
639
|
+
/* Avoid re-loading if git_root is the same as base_directory. */
|
|
640
|
+
if (!options->base_directory ||
|
|
641
|
+
strcmp(git_root, options->base_directory) != 0) {
|
|
642
|
+
char *git_proj_dir = dup_join(git_root, ".apex/plugins");
|
|
643
|
+
if (git_proj_dir) {
|
|
644
|
+
load_plugins_from_dir(manager, git_proj_dir);
|
|
645
|
+
free(git_proj_dir);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
607
648
|
}
|
|
608
|
-
|
|
649
|
+
free(git_root);
|
|
609
650
|
}
|
|
610
|
-
free(git_root);
|
|
611
|
-
}
|
|
612
651
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
char buf[1024];
|
|
618
|
-
snprintf(buf, sizeof(buf), "%s/apex/plugins", xdg);
|
|
619
|
-
global_dir = strdup(buf);
|
|
620
|
-
} else {
|
|
621
|
-
const char *home = getenv("HOME");
|
|
622
|
-
if (home && *home) {
|
|
652
|
+
/* User-global: $XDG_CONFIG_HOME/apex/plugins or $HOME/.config/apex/plugins */
|
|
653
|
+
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
654
|
+
char *global_dir = NULL;
|
|
655
|
+
if (xdg && *xdg) {
|
|
623
656
|
char buf[1024];
|
|
624
|
-
snprintf(buf, sizeof(buf), "%s
|
|
657
|
+
snprintf(buf, sizeof(buf), "%s/apex/plugins", xdg);
|
|
625
658
|
global_dir = strdup(buf);
|
|
659
|
+
} else {
|
|
660
|
+
const char *home = getenv("HOME");
|
|
661
|
+
if (home && *home) {
|
|
662
|
+
char buf[1024];
|
|
663
|
+
snprintf(buf, sizeof(buf), "%s/.config/apex/plugins", home);
|
|
664
|
+
global_dir = strdup(buf);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (global_dir) {
|
|
668
|
+
load_plugins_from_dir(manager, global_dir);
|
|
669
|
+
free(global_dir);
|
|
626
670
|
}
|
|
627
|
-
}
|
|
628
|
-
if (global_dir) {
|
|
629
|
-
load_plugins_from_dir(manager, global_dir);
|
|
630
|
-
free(global_dir);
|
|
631
671
|
}
|
|
632
672
|
|
|
633
673
|
if (!manager->pre_parse && !manager->post_render) {
|
|
@@ -861,6 +901,8 @@ char *apex_plugins_run_text_phase(apex_plugin_manager *manager,
|
|
|
861
901
|
}
|
|
862
902
|
} else if (p->has_regex) {
|
|
863
903
|
next = apply_regex_replacement(p, current);
|
|
904
|
+
} else if (p->callback) {
|
|
905
|
+
next = p->callback(current, p->id, phase, options);
|
|
864
906
|
}
|
|
865
907
|
|
|
866
908
|
if (do_profile) {
|
|
@@ -7,16 +7,6 @@
|
|
|
7
7
|
extern "C" {
|
|
8
8
|
#endif
|
|
9
9
|
|
|
10
|
-
/* Plugin phases */
|
|
11
|
-
typedef enum {
|
|
12
|
-
APEX_PLUGIN_PHASE_PRE_PARSE = 1 << 0,
|
|
13
|
-
APEX_PLUGIN_PHASE_BLOCK = 1 << 1,
|
|
14
|
-
APEX_PLUGIN_PHASE_INLINE = 1 << 2,
|
|
15
|
-
APEX_PLUGIN_PHASE_POST_RENDER= 1 << 3
|
|
16
|
-
} apex_plugin_phase_mask;
|
|
17
|
-
|
|
18
|
-
typedef struct apex_plugin_manager apex_plugin_manager;
|
|
19
|
-
|
|
20
10
|
/* Discover and load plugins from project and user config dirs.
|
|
21
11
|
* Returns NULL if no plugins are found or an error occurs. */
|
|
22
12
|
apex_plugin_manager *apex_plugins_load(const apex_options *options);
|
|
@@ -56,7 +56,7 @@ static char *extract_tag_name(const char *tag_start, bool *is_closing, bool *is_
|
|
|
56
56
|
const char *name_start = p;
|
|
57
57
|
while (*p && !isspace((unsigned char)*p) && *p != '>' && *p != '/') p++;
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
size_t len = (size_t)(p - name_start);
|
|
60
60
|
if (len == 0) return NULL;
|
|
61
61
|
|
|
62
62
|
char *name = malloc(len + 1);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# CLI tests for --info, --extract-meta, and --extract-meta-value using tests/fixtures/metadata.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
APEX_BIN="${APEX_BIN:-$ROOT/build/apex}"
|
|
9
|
+
FIXTURES="$ROOT/tests/fixtures/metadata"
|
|
10
|
+
YAML="$FIXTURES/yaml-frontmatter.md"
|
|
11
|
+
MMD="$FIXTURES/mmd-metadata.md"
|
|
12
|
+
PANDOC="$FIXTURES/pandoc-meta.md"
|
|
13
|
+
|
|
14
|
+
if [[ ! -x "$APEX_BIN" ]]; then
|
|
15
|
+
echo "Error: $APEX_BIN not found or not executable. Build Apex first (cmake --build build --target apex_cli)." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
for f in "$YAML" "$MMD" "$PANDOC"; do
|
|
20
|
+
if [[ ! -f "$f" ]]; then
|
|
21
|
+
echo "Error: missing fixture $f" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
TMPDIR="${TMPDIR:-/tmp}"
|
|
27
|
+
WORK="$(mktemp -d "${TMPDIR}/apex-metadata-cli-XXXXXX")"
|
|
28
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
29
|
+
|
|
30
|
+
echo "== Testing -i / --info (no input files; info on stdout) =="
|
|
31
|
+
|
|
32
|
+
INFO_OUT="$WORK/info_stdout.txt"
|
|
33
|
+
# Close stdin so the CLI does not wait for interactive input in some environments.
|
|
34
|
+
"$APEX_BIN" -i </dev/null >"$INFO_OUT"
|
|
35
|
+
grep -q '^version:' "$INFO_OUT"
|
|
36
|
+
grep -q 'plugins:' "$INFO_OUT"
|
|
37
|
+
echo "-i without files: stdout contains version and plugins section."
|
|
38
|
+
|
|
39
|
+
echo
|
|
40
|
+
echo "== Testing -i with a file (info on stderr, HTML on stdout) =="
|
|
41
|
+
|
|
42
|
+
STDOUT="$WORK/out.html"
|
|
43
|
+
STDERR="$WORK/info.err"
|
|
44
|
+
"$APEX_BIN" -i "$YAML" >"$STDOUT" 2>"$STDERR"
|
|
45
|
+
grep -q '^version:' "$STDERR" || {
|
|
46
|
+
echo "metadata_cli_test: expected version line on stderr when using -i with a file" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
}
|
|
49
|
+
grep -q '<p>' "$STDOUT" || {
|
|
50
|
+
echo "metadata_cli_test: expected HTML fragment on stdout with -i and a file" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
}
|
|
53
|
+
echo "-i with file: version/config on stderr, conversion on stdout."
|
|
54
|
+
|
|
55
|
+
echo
|
|
56
|
+
echo "== Testing --extract-meta (YAML fixture) =="
|
|
57
|
+
|
|
58
|
+
META_OUT="$WORK/meta.yaml"
|
|
59
|
+
"$APEX_BIN" --extract-meta "$YAML" >"$META_OUT"
|
|
60
|
+
grep -q '^title: A YAML test$' "$META_OUT" || {
|
|
61
|
+
echo "metadata_cli_test: --extract-meta missing expected title from YAML fixture" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
}
|
|
64
|
+
grep -q '^random_key: Flargle$' "$META_OUT" || {
|
|
65
|
+
echo "metadata_cli_test: --extract-meta missing random_key from YAML fixture" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
}
|
|
68
|
+
echo "--extract-meta on YAML front matter: expected keys present."
|
|
69
|
+
|
|
70
|
+
echo
|
|
71
|
+
echo "== Testing -e / --extract-meta-value =="
|
|
72
|
+
|
|
73
|
+
val="$("$APEX_BIN" -e title "$YAML")"
|
|
74
|
+
if [[ "$val" != "A YAML test" ]]; then
|
|
75
|
+
echo "metadata_cli_test: -e title from YAML expected 'A YAML test', got '$val'" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
val="$("$APEX_BIN" --extract-meta-value random_key "$YAML")"
|
|
80
|
+
if [[ "$val" != "Flargle" ]]; then
|
|
81
|
+
echo "metadata_cli_test: -e random_key expected 'Flargle', got '$val'" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
val="$("$APEX_BIN" -e randomkey "$MMD")"
|
|
86
|
+
if [[ "$val" != "Bargle" ]]; then
|
|
87
|
+
echo "metadata_cli_test: -e randomkey (MMD) expected 'Bargle', got '$val'" >&2
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
val="$("$APEX_BIN" -e title "$PANDOC")"
|
|
92
|
+
if [[ "$val" != "Pandoc Metadata" ]]; then
|
|
93
|
+
echo "metadata_cli_test: -e title (Pandoc) expected 'Pandoc Metadata', got '$val'" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if "$APEX_BIN" -e definitely_missing_key_xyz "$YAML" 2>"$WORK/miss.err"; then
|
|
98
|
+
echo "metadata_cli_test: expected non-zero exit for missing metadata key" >&2
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
grep -q "not found" "$WORK/miss.err" || {
|
|
102
|
+
echo "metadata_cli_test: expected error message for missing key on stderr" >&2
|
|
103
|
+
exit 1
|
|
104
|
+
}
|
|
105
|
+
echo "-e returns values and exit 1 when key is absent."
|
|
106
|
+
|
|
107
|
+
echo
|
|
108
|
+
echo "== Testing --combine --extract-meta (merge order, later file wins) =="
|
|
109
|
+
|
|
110
|
+
COMBINED="$WORK/combined_meta.yaml"
|
|
111
|
+
"$APEX_BIN" --combine --extract-meta "$MMD" "$YAML" >"$COMBINED"
|
|
112
|
+
grep -q '^title: A YAML test$' "$COMBINED" || {
|
|
113
|
+
echo "metadata_cli_test: combined extract expected last file to win for title" >&2
|
|
114
|
+
exit 1
|
|
115
|
+
}
|
|
116
|
+
echo "--combine --extract-meta: later file overrides title as expected."
|
|
117
|
+
|
|
118
|
+
echo
|
|
119
|
+
echo "All metadata CLI tests passed."
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Sbarex on 10/02/26.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#include "test_helpers.h"
|
|
6
|
+
#include "apex/apex.h"
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/* cmark-gfm headers */
|
|
11
|
+
#include "cmark-gfm.h"
|
|
12
|
+
#include <string.h>
|
|
13
|
+
|
|
14
|
+
static char * my_plugin_callback(const char *text, __attribute__((unused)) const char *id_plugin, apex_plugin_phase_mask phase_mask, __attribute__((unused)) const apex_options *options) {
|
|
15
|
+
if (phase_mask & APEX_PLUGIN_PHASE_PRE_PARSE) {
|
|
16
|
+
const char *suffix = "\n_Hello Sbarex_\n";
|
|
17
|
+
size_t len1 = strlen(text);
|
|
18
|
+
size_t len2 = strlen(suffix);
|
|
19
|
+
|
|
20
|
+
char *result = malloc(len1 + len2 + 1);
|
|
21
|
+
if (!result) return NULL;
|
|
22
|
+
|
|
23
|
+
memcpy(result, text, len1);
|
|
24
|
+
memcpy(result + len1, suffix, len2 + 1); // include '\0'
|
|
25
|
+
return result;
|
|
26
|
+
} else if (phase_mask & APEX_PLUGIN_PHASE_POST_RENDER) {
|
|
27
|
+
const char *suffix = "\n<p>Everything is fine</p>";
|
|
28
|
+
size_t len1 = strlen(text);
|
|
29
|
+
size_t len2 = strlen(suffix);
|
|
30
|
+
|
|
31
|
+
char *result = malloc(len1 + len2 + 1);
|
|
32
|
+
if (!result) return NULL;
|
|
33
|
+
|
|
34
|
+
memcpy(result, text, len1);
|
|
35
|
+
memcpy(result + len1, suffix, len2 + 1);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
return NULL;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static void my_plugin_register(apex_plugin_manager *manager, __attribute__((unused)) const apex_options *options) {
|
|
42
|
+
printf(COLOR_GREEN "✓" COLOR_RESET " Custom plugin register callback called\n");
|
|
43
|
+
|
|
44
|
+
/* Attach to appropriate phase lists, enforcing per-list id uniqueness */
|
|
45
|
+
if (apex_plugin_register(manager, "my_plugin", APEX_PLUGIN_PHASE_PRE_PARSE, my_plugin_callback)) {
|
|
46
|
+
printf(COLOR_GREEN "✓" COLOR_RESET " Custom plugin has been registered for pre parse\n");
|
|
47
|
+
} else {
|
|
48
|
+
printf(COLOR_RED "✗" COLOR_RESET " Custom plugin has not been registered for pre parse\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Attach to appropriate phase lists, enforcing per-list id uniqueness */
|
|
52
|
+
if (apex_plugin_register(manager, "my_plugin", APEX_PLUGIN_PHASE_POST_RENDER, my_plugin_callback)) {
|
|
53
|
+
printf(COLOR_GREEN "✓" COLOR_RESET " Custom plugin has been registered for post render\n");
|
|
54
|
+
} else {
|
|
55
|
+
printf(COLOR_RED "✗" COLOR_RESET " Custom plugin has not been registered for post render\n");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void test_custom_plugins(void) {
|
|
60
|
+
int suite_failures = suite_start();
|
|
61
|
+
print_suite_title("Custom plugins Tests", false, true);
|
|
62
|
+
|
|
63
|
+
apex_options opts = apex_options_default();
|
|
64
|
+
opts.enable_plugins = true;
|
|
65
|
+
opts.allow_external_plugin_detection = false;
|
|
66
|
+
opts.plugin_register = my_plugin_register;
|
|
67
|
+
|
|
68
|
+
const char *s = "# Custom plugin callback test\n\n";
|
|
69
|
+
|
|
70
|
+
char *html;
|
|
71
|
+
html = apex_markdown_to_html(s, strlen(s), &opts);
|
|
72
|
+
assert_contains(html, "<em>Hello Sbarex</em>", "Custom pre parse plugin executed");
|
|
73
|
+
assert_contains(html, "<p>Everything is fine</p>", "Custom post render plugin executed");
|
|
74
|
+
apex_free_string(html);
|
|
75
|
+
|
|
76
|
+
bool had_failures = suite_end(suite_failures);
|
|
77
|
+
print_suite_title("Cmark Callbacks Tests", had_failures, false);
|
|
78
|
+
}
|
|
@@ -2246,6 +2246,33 @@ void test_mixed_lists(void) {
|
|
|
2246
2246
|
}
|
|
2247
2247
|
apex_free_string(html);
|
|
2248
2248
|
|
|
2249
|
+
/* Regression: alpha lists with nested sublists should stay intact and not leak marker tokens */
|
|
2250
|
+
unified_opts = apex_options_for_mode(APEX_MODE_UNIFIED);
|
|
2251
|
+
const char *alpha_with_nested_bullets = "a. Test\nb. Test\n\t- Test\n\t- Test\nc. Test\n";
|
|
2252
|
+
html = apex_markdown_to_html(alpha_with_nested_bullets, strlen(alpha_with_nested_bullets), &unified_opts);
|
|
2253
|
+
assert_contains(html, "<ol style=\"list-style-type: lower-alpha\">", "Alpha list keeps lower-alpha style with nested bullets");
|
|
2254
|
+
assert_contains(html, "<ul>", "Nested bullet list rendered");
|
|
2255
|
+
assert_not_contains(html, "[apex-alpha-list:", "Alpha marker token removed from output");
|
|
2256
|
+
apex_free_string(html);
|
|
2257
|
+
|
|
2258
|
+
const char *alpha_with_nested_ordered = "a. Test\nb. Test\n\t1. Nested one\n\t2. Nested two\nc. Test\n";
|
|
2259
|
+
html = apex_markdown_to_html(alpha_with_nested_ordered, strlen(alpha_with_nested_ordered), &unified_opts);
|
|
2260
|
+
assert_contains(html, "<ol style=\"list-style-type: lower-alpha\">", "Alpha list keeps style with nested ordered list");
|
|
2261
|
+
assert_not_contains(html, "[apex-alpha-list:", "No leaked alpha marker token after nested ordered list");
|
|
2262
|
+
apex_free_string(html);
|
|
2263
|
+
|
|
2264
|
+
const char *alpha_with_nested_alpha = "a. Test\nb. Test\n\tc. Test\n\td. Test\ne. Test\n";
|
|
2265
|
+
html = apex_markdown_to_html(alpha_with_nested_alpha, strlen(alpha_with_nested_alpha), &unified_opts);
|
|
2266
|
+
assert_contains(html, "<ol style=\"list-style-type: lower-alpha\">", "Alpha list keeps style with nested alpha sublist");
|
|
2267
|
+
assert_not_contains(html, "[apex-alpha-list:", "No leaked alpha marker token after nested alpha sublist");
|
|
2268
|
+
apex_free_string(html);
|
|
2269
|
+
|
|
2270
|
+
const char *numeric_with_nested_ordered = "1. Test\n2. Test\n\t3. Test\n\t4. Test\n5. Test\n";
|
|
2271
|
+
html = apex_markdown_to_html(numeric_with_nested_ordered, strlen(numeric_with_nested_ordered), &unified_opts);
|
|
2272
|
+
assert_contains(html, "<ol start=\"3\">", "Numeric nested ordered sublist renders as nested ordered list");
|
|
2273
|
+
assert_not_contains(html, "2. Test\n 3. Test", "Nested ordered items are not flattened into parent text");
|
|
2274
|
+
apex_free_string(html);
|
|
2275
|
+
|
|
2249
2276
|
bool had_failures = suite_end(suite_failures);
|
|
2250
2277
|
print_suite_title("Mixed List Markers Tests", had_failures, false);
|
|
2251
2278
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
#include "../src/extensions/metadata.h"
|
|
8
8
|
#include <string.h>
|
|
9
9
|
#include <stdlib.h>
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <assert.h>
|
|
10
12
|
|
|
11
13
|
void test_metadata(void) {
|
|
12
14
|
int suite_failures = suite_start();
|
|
@@ -96,6 +98,46 @@ void test_metadata(void) {
|
|
|
96
98
|
print_suite_title("Metadata Tests", had_failures, false);
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* YAML serialization helpers used by the CLI
|
|
103
|
+
*/
|
|
104
|
+
void test_metadata_yaml_emit(void) {
|
|
105
|
+
int suite_failures = suite_start();
|
|
106
|
+
print_suite_title("Metadata YAML emit Tests", false, true);
|
|
107
|
+
|
|
108
|
+
apex_metadata_item *m = apex_parse_command_metadata("title=Hello World");
|
|
109
|
+
assert(m != NULL);
|
|
110
|
+
FILE *fp = tmpfile();
|
|
111
|
+
assert(fp != NULL);
|
|
112
|
+
apex_metadata_fprint_yaml_document(fp, m);
|
|
113
|
+
rewind(fp);
|
|
114
|
+
char buf[800];
|
|
115
|
+
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
|
116
|
+
buf[n] = '\0';
|
|
117
|
+
fclose(fp);
|
|
118
|
+
assert_contains(buf, "---", "yaml document has markers");
|
|
119
|
+
assert_contains(buf, "title:", "yaml title key");
|
|
120
|
+
assert_contains(buf, "Hello World", "yaml title value");
|
|
121
|
+
apex_free_metadata(m);
|
|
122
|
+
|
|
123
|
+
apex_metadata_item *a = apex_parse_command_metadata("x=1");
|
|
124
|
+
apex_metadata_item *b = apex_parse_command_metadata("x=2");
|
|
125
|
+
apex_metadata_item *mg = apex_merge_metadata(a, b, NULL);
|
|
126
|
+
apex_free_metadata(a);
|
|
127
|
+
apex_free_metadata(b);
|
|
128
|
+
fp = tmpfile();
|
|
129
|
+
apex_metadata_fprint_yaml_mapping(fp, mg);
|
|
130
|
+
rewind(fp);
|
|
131
|
+
n = fread(buf, 1, sizeof(buf) - 1, fp);
|
|
132
|
+
buf[n] = '\0';
|
|
133
|
+
fclose(fp);
|
|
134
|
+
assert_contains(buf, "x: 2", "merge: later metadata wins for yaml mapping");
|
|
135
|
+
apex_free_metadata(mg);
|
|
136
|
+
|
|
137
|
+
bool had_failures = suite_end(suite_failures);
|
|
138
|
+
print_suite_title("Metadata YAML emit Tests", had_failures, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
99
141
|
/**
|
|
100
142
|
* Test MultiMarkdown metadata keys
|
|
101
143
|
*/
|
|
@@ -34,6 +34,13 @@ void test_toc(void) {
|
|
|
34
34
|
assert_contains(html, "Section", "MMD TOC includes headers");
|
|
35
35
|
apex_free_string(html);
|
|
36
36
|
|
|
37
|
+
/* MultiMarkdown mode should process {{TOC}} even with marked extensions disabled */
|
|
38
|
+
apex_options mmd_opts = apex_options_for_mode(APEX_MODE_MULTIMARKDOWN);
|
|
39
|
+
html = apex_markdown_to_html(mmd_toc, strlen(mmd_toc), &mmd_opts);
|
|
40
|
+
assert_contains(html, "<nav class=\"toc\">", "MMD mode renders TOC marker");
|
|
41
|
+
assert_contains(html, "Section", "MMD mode TOC includes headers");
|
|
42
|
+
apex_free_string(html);
|
|
43
|
+
|
|
37
44
|
/* Test TOC with depth range */
|
|
38
45
|
const char *depth_toc = "# H1\n\n{{TOC:2-3}}\n\n## H2\n\n### H3\n\n#### H4";
|
|
39
46
|
html = apex_markdown_to_html(depth_toc, strlen(depth_toc), &opts);
|
|
@@ -390,6 +397,82 @@ void test_terminal_output(void) {
|
|
|
390
397
|
test_result(out != NULL && strstr(out, "plain") != NULL, "terminal_width set still produces terminal output");
|
|
391
398
|
if (out) apex_free_string(out);
|
|
392
399
|
|
|
400
|
+
/* apex_resolve_local_image_path */
|
|
401
|
+
{
|
|
402
|
+
char *rp = apex_resolve_local_image_path("img/a.png", "/tmp/proj");
|
|
403
|
+
test_result(rp != NULL && strcmp(rp, "/tmp/proj/img/a.png") == 0,
|
|
404
|
+
"apex_resolve_local_image_path joins base_directory");
|
|
405
|
+
free(rp);
|
|
406
|
+
rp = apex_resolve_local_image_path("/abs/foo.png", "/any");
|
|
407
|
+
test_result(rp != NULL && strcmp(rp, "/abs/foo.png") == 0,
|
|
408
|
+
"apex_resolve_local_image_path keeps absolute paths");
|
|
409
|
+
free(rp);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* Terminal images: inline rendering uses isatty(STDOUT_FILENO). Test output is
|
|
413
|
+
* captured to a string; under a normal CI pipe or non-TTY, stdout is not a TTY
|
|
414
|
+
* so these exercises never call curl or imgcat/chafa/viu/catimg. (Running the
|
|
415
|
+
* suite in an interactive terminal with those tools on PATH could take a
|
|
416
|
+
* different path for remote URLs.) */
|
|
417
|
+
|
|
418
|
+
/* Remote images: link-style fallback when not inline (non-TTY: no download/viewer) */
|
|
419
|
+
opts = apex_options_default();
|
|
420
|
+
opts.output_format = APEX_OUTPUT_TERMINAL;
|
|
421
|
+
out = apex_markdown_to_html("", 28, &opts);
|
|
422
|
+
assert_contains(out, "https://ex.com/x.png", "Remote image URL in fallback");
|
|
423
|
+
test_result(strstr(out, "", 26, &opts);
|
|
430
|
+
assert_contains(out, "http://ex.com/y.png", "http remote image URL in fallback");
|
|
431
|
+
test_result(strstr(out, "", 28, &opts);
|
|
438
|
+
assert_contains(out, "https://ex.com/z.png", "terminal256 remote image URL in fallback");
|
|
439
|
+
test_result(strstr(out, "", 25, &opts);
|
|
446
|
+
assert_contains(out, "https://ex.com/e.png", "Empty-alt remote image URL in fallback");
|
|
447
|
+
test_result(strstr(out, "", 28, &opts);
|
|
455
|
+
test_result(out != NULL && strstr(out, "https://ex.com/w.png") != NULL,
|
|
456
|
+
"terminal_image_width set with non-TTY still yields link-style remote image");
|
|
457
|
+
apex_free_string(out);
|
|
458
|
+
|
|
459
|
+
/* terminal_inline_images false: link-style fallback */
|
|
460
|
+
opts = apex_options_default();
|
|
461
|
+
opts.output_format = APEX_OUTPUT_TERMINAL;
|
|
462
|
+
opts.terminal_inline_images = false;
|
|
463
|
+
out = apex_markdown_to_html("", 17, &opts);
|
|
464
|
+
assert_contains(out, "local.png", "Disabled inline: URL shown like a link");
|
|
465
|
+
test_result(strstr(out, "", 45, &opts);
|
|
472
|
+
assert_contains(out, "apex_test_missing_image_99.png", "Missing file: URL in link-style output");
|
|
473
|
+
test_result(strstr(out, "![") == NULL, "Missing local image uses link style not markdown image");
|
|
474
|
+
apex_free_string(out);
|
|
475
|
+
|
|
393
476
|
bool had_failures = suite_end(suite_failures);
|
|
394
477
|
print_suite_title("Terminal Output Tests", had_failures, false);
|
|
395
478
|
}
|