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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/ext/apex_ext/apex_src/CHANGELOG.md +69 -0
  3. data/ext/apex_ext/apex_src/CMakeLists.txt +2 -1
  4. data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
  5. data/ext/apex_ext/apex_src/Package.swift +14 -2
  6. data/ext/apex_ext/apex_src/README.md +12 -9
  7. data/ext/apex_ext/apex_src/VERSION +1 -1
  8. data/ext/apex_ext/apex_src/cli/main.c +625 -98
  9. data/ext/apex_ext/apex_src/ial.html +24 -0
  10. data/ext/apex_ext/apex_src/include/apex/apex.h +57 -7
  11. data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +3 -0
  12. data/ext/apex_ext/apex_src/include/apex/module.modulemap +8 -0
  13. data/ext/apex_ext/apex_src/include/apexc.h +6 -0
  14. data/ext/apex_ext/apex_src/include/module.modulemap +4 -0
  15. data/ext/apex_ext/apex_src/man/apex-config.5 +8 -2
  16. data/ext/apex_ext/apex_src/man/apex-plugins.7 +13 -13
  17. data/ext/apex_ext/apex_src/man/apex.1 +150 -442
  18. data/ext/apex_ext/apex_src/man/apex.1.md +13 -0
  19. data/ext/apex_ext/apex_src/src/_README.md +3 -1
  20. data/ext/apex_ext/apex_src/src/apex.c +151 -6
  21. data/ext/apex_ext/apex_src/src/ast_terminal.c +459 -8
  22. data/ext/apex_ext/apex_src/src/extensions/advanced_tables.c +6 -6
  23. data/ext/apex_ext/apex_src/src/extensions/callouts.c +1 -1
  24. data/ext/apex_ext/apex_src/src/extensions/citations.c +24 -12
  25. data/ext/apex_ext/apex_src/src/extensions/critic.c +14 -6
  26. data/ext/apex_ext/apex_src/src/extensions/emoji.c +2 -2
  27. data/ext/apex_ext/apex_src/src/extensions/grid_tables.c +1 -1
  28. data/ext/apex_ext/apex_src/src/extensions/header_ids.c +19 -6
  29. data/ext/apex_ext/apex_src/src/extensions/ial.c +25 -13
  30. data/ext/apex_ext/apex_src/src/extensions/includes.c +7 -7
  31. data/ext/apex_ext/apex_src/src/extensions/index.c +19 -7
  32. data/ext/apex_ext/apex_src/src/extensions/inline_footnotes.c +2 -2
  33. data/ext/apex_ext/apex_src/src/extensions/insert.c +1 -1
  34. data/ext/apex_ext/apex_src/src/extensions/math.c +11 -2
  35. data/ext/apex_ext/apex_src/src/extensions/metadata.c +46 -0
  36. data/ext/apex_ext/apex_src/src/extensions/metadata.h +12 -0
  37. data/ext/apex_ext/apex_src/src/html_renderer.c +2 -2
  38. data/ext/apex_ext/apex_src/src/plugins.c +97 -55
  39. data/ext/apex_ext/apex_src/src/plugins.h +0 -10
  40. data/ext/apex_ext/apex_src/src/pretty_html.c +1 -1
  41. data/ext/apex_ext/apex_src/tests/fixtures/metadata/mmd-metadata.md +5 -0
  42. data/ext/apex_ext/apex_src/tests/fixtures/metadata/pandoc-meta.md +4 -0
  43. data/ext/apex_ext/apex_src/tests/fixtures/metadata/yaml-frontmatter.md +6 -0
  44. data/ext/apex_ext/apex_src/tests/metadata_cli_test.sh +119 -0
  45. data/ext/apex_ext/apex_src/tests/test_custom_plugins.c +78 -0
  46. data/ext/apex_ext/apex_src/tests/test_extensions.c +27 -0
  47. data/ext/apex_ext/apex_src/tests/test_metadata.c +42 -0
  48. data/ext/apex_ext/apex_src/tests/test_output.c +83 -0
  49. data/ext/apex_ext/apex_src/tests/test_runner.c +4 -1
  50. data/lib/apex/version.rb +1 -1
  51. metadata +10 -2
@@ -92,7 +92,8 @@ static char *apex_git_toplevel(void) {
92
92
  return out;
93
93
  }
94
94
 
95
- struct apex_plugin {
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
- char *handler_command;
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 = calloc(1, sizeof(struct apex_plugin));
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 = calloc(1, sizeof(struct apex_plugin));
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
- /* Project-scoped (current working directory): CWD/.apex/plugins */
572
- char cwd[1024];
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
- /* Project-scoped (explicit base_directory): base_directory/.apex/plugins */
583
- if (options->base_directory && options->base_directory[0] != '\0') {
584
- char *proj_dir = dup_join(options->base_directory, ".apex/plugins");
585
- if (proj_dir) {
586
- load_plugins_from_dir(manager, proj_dir);
587
- free(proj_dir);
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
- /* Project-scoped (Git repository root): <git top>/\.apex/plugins
592
- * Only used when the current directory is inside the work tree.
593
- */
594
- char *git_root = apex_git_toplevel();
595
- if (git_root && git_root[0] != '\0' && cwd[0] != '\0') {
596
- size_t root_len = strlen(git_root);
597
- /* Ensure the Git root is a parent of (or equal to) the current directory. */
598
- if (strncmp(cwd, git_root, root_len) == 0 &&
599
- (cwd[root_len] == '/' || cwd[root_len] == '\0')) {
600
- /* Avoid re-loading if git_root is the same as base_directory. */
601
- if (!options->base_directory ||
602
- strcmp(git_root, options->base_directory) != 0) {
603
- char *git_proj_dir = dup_join(git_root, ".apex/plugins");
604
- if (git_proj_dir) {
605
- load_plugins_from_dir(manager, git_proj_dir);
606
- free(git_proj_dir);
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
- /* User-global: $XDG_CONFIG_HOME/apex/plugins or $HOME/.config/apex/plugins */
614
- const char *xdg = getenv("XDG_CONFIG_HOME");
615
- char *global_dir = NULL;
616
- if (xdg && *xdg) {
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/.config/apex/plugins", home);
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
- int len = p - name_start;
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,5 @@
1
+ Title: A MultiMarkdown Test
2
+ Author: Brett Terpstra
3
+ Random Key: Bargle
4
+
5
+ This is a test of MultiMarkdown metadata. [%randomkey]
@@ -0,0 +1,4 @@
1
+ % Pandoc Metadata
2
+ % Brett Terpstra
3
+
4
+ This is a test of Pandoc metadata.
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: A YAML test
3
+ author: Brett Terpstra
4
+ random_key: Flargle
5
+ ---
6
+ This is a test of YAML Front Matter. [%randomkey]
@@ -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("![r](https://ex.com/x.png)", 28, &opts);
422
+ assert_contains(out, "https://ex.com/x.png", "Remote image URL in fallback");
423
+ test_result(strstr(out, "![") == NULL, "Remote image fallback uses link style not markdown image");
424
+ apex_free_string(out);
425
+
426
+ /* http:// same as https for fallback */
427
+ opts = apex_options_default();
428
+ opts.output_format = APEX_OUTPUT_TERMINAL;
429
+ out = apex_markdown_to_html("![h](http://ex.com/y.png)", 26, &opts);
430
+ assert_contains(out, "http://ex.com/y.png", "http remote image URL in fallback");
431
+ test_result(strstr(out, "![") == NULL, "http remote image uses link style not markdown image");
432
+ apex_free_string(out);
433
+
434
+ /* terminal256: same link-style fallback for images */
435
+ opts = apex_options_default();
436
+ opts.output_format = APEX_OUTPUT_TERMINAL256;
437
+ out = apex_markdown_to_html("![z](https://ex.com/z.png)", 28, &opts);
438
+ assert_contains(out, "https://ex.com/z.png", "terminal256 remote image URL in fallback");
439
+ test_result(strstr(out, "![") == NULL, "terminal256 remote image uses link style");
440
+ apex_free_string(out);
441
+
442
+ /* Empty alt still emits (url) */
443
+ opts = apex_options_default();
444
+ opts.output_format = APEX_OUTPUT_TERMINAL;
445
+ out = apex_markdown_to_html("![](https://ex.com/e.png)", 25, &opts);
446
+ assert_contains(out, "https://ex.com/e.png", "Empty-alt remote image URL in fallback");
447
+ test_result(strstr(out, "![") == NULL, "Empty-alt image uses link style");
448
+ apex_free_string(out);
449
+
450
+ /* terminal_image_width is ignored when not inlining (non-TTY); must not crash */
451
+ opts = apex_options_default();
452
+ opts.output_format = APEX_OUTPUT_TERMINAL;
453
+ opts.terminal_image_width = 72;
454
+ out = apex_markdown_to_html("![w](https://ex.com/w.png)", 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("![z](local.png)", 17, &opts);
464
+ assert_contains(out, "local.png", "Disabled inline: URL shown like a link");
465
+ test_result(strstr(out, "![") == NULL, "terminal_inline_images off uses link style not markdown image");
466
+ apex_free_string(out);
467
+
468
+ /* Missing local file: link-style fallback (non-TTY) */
469
+ opts = apex_options_default();
470
+ opts.output_format = APEX_OUTPUT_TERMINAL;
471
+ out = apex_markdown_to_html("![missing](_/apex_test_missing_image_99.png)", 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
  }