apex-ruby 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/ext/apex_ext/apex_ext.c +6 -0
  3. data/ext/apex_ext/apex_src/AGENTS.md +41 -0
  4. data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
  5. data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
  6. data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
  7. data/ext/apex_ext/apex_src/Package.swift +9 -0
  8. data/ext/apex_ext/apex_src/README.md +31 -9
  9. data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
  10. data/ext/apex_ext/apex_src/VERSION +1 -1
  11. data/ext/apex_ext/apex_src/cli/main.c +1125 -13
  12. data/ext/apex_ext/apex_src/docs/index.md +459 -0
  13. data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
  14. data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
  15. data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
  16. data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
  17. data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
  18. data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
  19. data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
  20. data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
  21. data/ext/apex_ext/apex_src/man/apex.1 +663 -620
  22. data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
  23. data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
  24. data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
  25. data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
  26. data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
  27. data/ext/apex_ext/apex_src/pages/index.md +459 -0
  28. data/ext/apex_ext/apex_src/src/_README.md +4 -4
  29. data/ext/apex_ext/apex_src/src/apex.c +702 -44
  30. data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
  31. data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
  32. data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
  33. data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
  34. data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
  35. data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
  36. data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
  37. data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
  38. data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
  39. data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
  40. data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
  41. data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
  42. data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
  43. data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
  44. data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
  45. data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
  46. data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
  47. data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
  48. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
  49. data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
  50. data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
  51. data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
  52. data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
  53. data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
  54. data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
  55. data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
  56. data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
  57. data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
  58. data/ext/apex_ext/apex_src/tests/README.md +11 -5
  59. data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
  60. data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
  61. data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
  62. data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
  63. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
  64. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
  65. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
  66. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
  67. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
  68. data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
  69. data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
  70. data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
  71. data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
  72. data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
  73. data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
  74. data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
  75. data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
  76. data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
  77. data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
  78. data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
  79. data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
  80. data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
  81. data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
  82. data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
  83. data/lib/apex/version.rb +1 -1
  84. metadata +32 -2
  85. data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +0 -456
@@ -118,6 +118,27 @@ char *apex_apply_aria_labels(const char *html, cmark_node *document);
118
118
  */
119
119
  char *apex_convert_image_captions(const char *html, bool enable_image_captions, bool title_captions_only);
120
120
 
121
+ /**
122
+ * Strip <p> that wraps only a single <img> (and optional leading "&lt; ") inside
123
+ * <figure>, so the result is <figure><img...></figure>. Call after image captions.
124
+ */
125
+ char *apex_strip_figure_paragraph_wrapper(const char *html);
126
+
127
+ /**
128
+ * Strip <p> that wraps only a single block element (figure, video, picture).
129
+ * HTML5 invalid: <p> may only contain phrasing content. Call after image captions.
130
+ */
131
+ char *apex_strip_block_paragraph_wrapper(const char *html);
132
+
133
+ /**
134
+ * Expand img tags with data-apex-replace-auto=1 by discovering existing
135
+ * format variants (2x, 3x, webp, avif, video formats) on disk.
136
+ * Only processes local relative URLs when base_directory is provided.
137
+ * @param html The HTML to process
138
+ * @param base_directory Base path for resolving relative URLs (e.g. document directory)
139
+ * @return Newly allocated HTML with auto media expanded (must be freed)
140
+ */
141
+ char *apex_expand_auto_media(const char *html, const char *base_directory);
121
142
  #ifdef __cplusplus
122
143
  }
123
144
  #endif
@@ -37,7 +37,7 @@ void apex_remote_free_plugins(apex_remote_plugin_list *list) {
37
37
  }
38
38
 
39
39
  /* Fetch JSON from a URL using curl. Returns malloc'd buffer or NULL. */
40
- static char *apex_remote_fetch_json(const char *url) {
40
+ char *apex_remote_fetch_json(const char *url) {
41
41
  if (!url) return NULL;
42
42
  /* Use curl -fsSL to fail on HTTP errors and be quiet except for data. */
43
43
  char cmd[1024];
@@ -85,7 +85,7 @@ static char *apex_remote_fetch_json(const char *url) {
85
85
  /* Very small JSON helper: extract string value for a key from an object snippet.
86
86
  * Assumes JSON is well-formed and keys/values are double-quoted.
87
87
  */
88
- static char *apex_remote_extract_string(const char *obj, const char *key) {
88
+ char *apex_remote_extract_string(const char *obj, const char *key) {
89
89
  if (!obj || !key) return NULL;
90
90
  char pattern[128];
91
91
  snprintf(pattern, sizeof(pattern), "\"%s\"", key);
@@ -114,15 +114,11 @@ static char *apex_remote_extract_string(const char *obj, const char *key) {
114
114
  return out;
115
115
  }
116
116
 
117
- /* Parse a very small subset of JSON: { \"plugins\": [ { ... }, { ... } ] } */
118
- static apex_remote_plugin_list *apex_remote_parse_directory(const char *json) {
119
- if (!json) return NULL;
120
- const char *plugins_key = "\"plugins\"";
121
- const char *p = strstr(json, plugins_key);
122
- if (!p) {
123
- fprintf(stderr, "Error: plugin directory JSON missing \"plugins\" key.\n");
124
- return NULL;
125
- }
117
+ /* Parse array of objects from JSON; array_key is e.g. "\"plugins\"" or "\"filters\"" */
118
+ static apex_remote_plugin_list *apex_remote_parse_array(const char *json, const char *array_key) {
119
+ if (!json || !array_key) return NULL;
120
+ const char *p = strstr(json, array_key);
121
+ if (!p) return NULL;
126
122
  p = strchr(p, '[');
127
123
  if (!p) return NULL;
128
124
  p++; /* move past '[' */
@@ -195,6 +191,27 @@ static apex_remote_plugin_list *apex_remote_parse_directory(const char *json) {
195
191
  return list;
196
192
  }
197
193
 
194
+ /* Parse { \"plugins\": [ ... ] } */
195
+ static apex_remote_plugin_list *apex_remote_parse_directory(const char *json) {
196
+ if (!json) return NULL;
197
+ if (!strstr(json, "\"plugins\"")) {
198
+ fprintf(stderr, "Error: plugin directory JSON missing \"plugins\" key.\n");
199
+ return NULL;
200
+ }
201
+ return apex_remote_parse_array(json, "\"plugins\"");
202
+ }
203
+
204
+ /* Parse { \"filters\": [ ... ] } - same shape as plugins (id, title, description, author, homepage, repo) */
205
+ static apex_remote_plugin_list *apex_remote_parse_filters_directory(const char *json) {
206
+ if (!json) return NULL;
207
+ const char *p = strstr(json, "\"filters\"");
208
+ if (!p) {
209
+ fprintf(stderr, "Error: filter directory JSON missing \"filters\" key.\n");
210
+ return NULL;
211
+ }
212
+ return apex_remote_parse_array(json, "\"filters\"");
213
+ }
214
+
198
215
  /* Public helpers used by CLI */
199
216
 
200
217
  apex_remote_plugin_list *apex_remote_fetch_directory(const char *url) {
@@ -205,11 +222,20 @@ apex_remote_plugin_list *apex_remote_fetch_directory(const char *url) {
205
222
  return list;
206
223
  }
207
224
 
225
+ apex_remote_plugin_list *apex_remote_fetch_filters_directory(const char *url) {
226
+ char *json = apex_remote_fetch_json(url);
227
+ if (!json) return NULL;
228
+ apex_remote_plugin_list *list = apex_remote_parse_filters_directory(json);
229
+ free(json);
230
+ return list;
231
+ }
232
+
208
233
  void apex_remote_print_plugins_filtered(apex_remote_plugin_list *list,
209
234
  const char **installed_ids,
210
- size_t installed_count) {
235
+ size_t installed_count,
236
+ const char *noun) {
211
237
  if (!list || !list->head) {
212
- fprintf(stderr, "No plugins found in remote directory.\n");
238
+ fprintf(stderr, "No %s found in remote directory.\n", noun ? noun : "plugins");
213
239
  return;
214
240
  }
215
241
  for (apex_remote_plugin *p = list->head; p; p = p->next) {
@@ -243,7 +269,7 @@ void apex_remote_print_plugins_filtered(apex_remote_plugin_list *list,
243
269
  }
244
270
 
245
271
  void apex_remote_print_plugins(apex_remote_plugin_list *list) {
246
- apex_remote_print_plugins_filtered(list, NULL, 0);
272
+ apex_remote_print_plugins_filtered(list, NULL, 0, NULL);
247
273
  }
248
274
 
249
275
  apex_remote_plugin *apex_remote_find_plugin(apex_remote_plugin_list *list, const char *id) {
@@ -5,6 +5,7 @@ add_executable(test_runner
5
5
  test_helpers.c
6
6
  test_runner.c
7
7
  test_basic.c
8
+ test_cmark_callback.c
8
9
  test_metadata.c
9
10
  test_links.c
10
11
  test_tables.c
@@ -85,22 +85,28 @@ Notes:
85
85
  - Depth control
86
86
  - Nested structure
87
87
 
88
- 14. **HTML Markdown Attributes** (9 tests) ✨ NEW
88
+ 14. **Terminal Output** (12 tests)
89
+ - ANSI terminal and terminal256 format
90
+ - List markers (bullet and ordered, default list_marker styling)
91
+ - terminal_width option (library)
92
+ - Optional script: `./tests/terminal_width_test.sh` checks CLI `--width` wrapping
93
+
94
+ 15. **HTML Markdown Attributes** (9 tests) ✨ NEW
89
95
  - markdown="1", "block", "span", "0"
90
96
  - Nested HTML parsing
91
97
 
92
- 15. **Abbreviations** (4 tests) ✨ NEW
98
+ 16. **Abbreviations** (4 tests) ✨ NEW
93
99
  - Definition syntax (partial support)
94
100
 
95
- 16. **Emoji** (10 tests) ✨ NEW
101
+ 17. **Emoji** (10 tests) ✨ NEW
96
102
  - 350+ GitHub emoji
97
103
  - Unknown emoji handling
98
104
 
99
- 17. **Special Markers** (7 tests) ✨ NEW
105
+ 18. **Special Markers** (7 tests) ✨ NEW
100
106
  - Page breaks, pauses
101
107
  - End-of-block markers
102
108
 
103
- 18. **Advanced Footnotes** (3 tests) ✨ NEW
109
+ 19. **Advanced Footnotes** (3 tests) ✨ NEW
104
110
  - Basic and inline footnotes
105
111
  - Markdown in footnotes
106
112
 
@@ -193,8 +193,6 @@ Use `^^` to merge cells vertically (rowspan):
193
193
 
194
194
  #### Combined Spans Example
195
195
 
196
- This table combines both rowspan and colspan features, as well as per-cell alignment:
197
-
198
196
  [Employee Performance Q4 2025]
199
197
 
200
198
  | Department | Employee | Q1-Q2 Average | Q3 | Q4 | Overall |
@@ -257,6 +255,19 @@ CommonMark
257
255
 
258
256
  And more!
259
257
 
258
+ **With blank lines:** A term can have blank lines before the first definition and between definitions:
259
+
260
+ Glossary
261
+ : First meaning of the term.
262
+
263
+ : Second meaning, after a blank line.
264
+
265
+ **One-line format:**
266
+
267
+ key::value
268
+
269
+ abbrev :: expanded form
270
+
260
271
  ## Critic Markup
261
272
 
262
273
  Here's some text with {++additions++} and {--deletions--}.
@@ -0,0 +1 @@
1
+ {"pandoc-api-version":[1,23,1],"meta":{},"blocks":[{"t":"Header","c":[1,["",[],[]],[{"t":"Str","c":"UNWRAP FILTER"}]},{"t":"RawBlock","c":["html","<figure><p>&lt; <img src=\"image.png\" alt=\"Image\" /></p>\n</figure>\n"]}]}
@@ -0,0 +1,7 @@
1
+ # Unwrap filter
2
+
3
+ What about this?
4
+
5
+ ::: >figure
6
+ < ![Image](image.png)
7
+ :::
@@ -0,0 +1,8 @@
1
+
2
+ ## Wildcard extension (*) - same as auto
3
+
4
+ Using `*` as the extension (e.g. `![](image.*)`) is equivalent to `![](image.png)` with the `auto` attribute when base_directory is set.
5
+
6
+ Apex scans for jpg, png, gif, webp, avif (1x, 2x, 3x) for images and mp4, webm, ogg, mov, m4v for videos.
7
+
8
+ ![Profile menu wildcard](img/app-pass-1-profile-menu.*)
@@ -0,0 +1,63 @@
1
+ # Media Format Handling - Images and Videos
2
+
3
+ This fixture tests WebP, AVIF, and video format attributes for images and videos.
4
+
5
+ ## Images with WebP
6
+
7
+ ![Hero image](img/hero.png "Hero!" webp)
8
+
9
+ ## Images with AVIF
10
+
11
+ ![Hero AVIF](img/hero.png avif)
12
+
13
+ ## Images with WebP and @2x
14
+
15
+ ![Retina WebP](img/hero.png webp @2x)
16
+
17
+ ## Images with AVIF and @2x
18
+
19
+ ![Retina AVIF](img/hero.png avif @2x )
20
+
21
+ ## Images with both WebP and AVIF
22
+
23
+ ![Modern formats](img/banner.jpg webp avif)
24
+
25
+ ## Video - basic MP4
26
+
27
+ ![Demo video](media/demo.mp4)
28
+
29
+ ## Video - MP4 with WebM alternative
30
+
31
+ ![Demo with WebM](media/demo.mp4 webm)
32
+
33
+ ## Video - MP4 with OGG alternative
34
+
35
+ ![Demo with OGG](media/intro.mp4 ogg)
36
+
37
+ ## Video - WebM with MP4 fallback
38
+
39
+ ![WebM primary](media/clip.webm mp4)
40
+
41
+ ## Video - MOV format
42
+
43
+ ![QuickTime](assets/trailer.mov)
44
+
45
+ ## Video - M4V format
46
+
47
+ ![M4V](assets/sample.m4v)
48
+
49
+ ## Auto - discover formats from filesystem
50
+
51
+ When `auto` is specified and base_directory is set, Apex discovers existing
52
+ variants (2x, 3x, webp, avif for images; webm, ogg, mp4, mov, m4v for videos)
53
+ and generates appropriate picture/video elements.
54
+
55
+ ![Profile menu](img/app-pass-1-profile-menu.jpg auto)
56
+
57
+ ## Wildcard extension (*) - same as auto
58
+
59
+ Using `*` as the extension (e.g. `![](image.*)`) is equivalent to `![](image.png)` with the `auto` attribute when base_directory is set.
60
+
61
+ Apex scans for jpg, png, gif, webp, avif (1x, 2x, 3x) for images and mp4, webm, ogg, mov, m4v for videos.
62
+
63
+ ![Profile menu wildcard](img/app-pass-1-profile-menu.*)
@@ -0,0 +1,3 @@
1
+ Name;Age;City
2
+ Alice;30;New York
3
+ Bob;25;San Francisco
@@ -0,0 +1 @@
1
+ Percent-decoded include works.
@@ -35,4 +35,7 @@ delta\tepsilon\tzeta
35
35
 
36
36
  No data here; should be unchanged.
37
37
 
38
-
38
+ <!--TABLE-->
39
+ one,two,three four five
40
+ :--,--:,--:
41
+ 1,23,245
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # Sanity checks for -p/--paginate and paginate config option for terminal output.
3
+ set -euo pipefail
4
+
5
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ APEX_BIN="${APEX_BIN:-$ROOT/build/apex}"
7
+
8
+ if [[ ! -x "$APEX_BIN" ]]; then
9
+ echo "Error: $APEX_BIN not found or not executable. Set APEX_BIN or build first." >&2
10
+ exit 1
11
+ fi
12
+
13
+ DOC="# Heading
14
+
15
+ Some *styled* text for testing."
16
+
17
+ echo "== Testing --paginate with APEX_PAGER=cat =="
18
+
19
+ BASE_OUT=$("$APEX_BIN" -t terminal <<< "$DOC")
20
+ PAGED_OUT=$(APEX_PAGER=cat "$APEX_BIN" -t terminal --paginate <<< "$DOC")
21
+
22
+ if [[ "$BASE_OUT" != "$PAGED_OUT" ]]; then
23
+ echo "paginate_cli_test: -p output differs from baseline when using APEX_PAGER=cat" >&2
24
+ exit 1
25
+ fi
26
+
27
+ echo "paginate_cli_test: -p with APEX_PAGER=cat matches baseline output."
28
+
29
+ echo
30
+ echo "== Testing paginate: true via config metadata with APEX_PAGER=cat =="
31
+
32
+ TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/apex-paginate-XXXXXX")"
33
+ trap 'rm -rf "$TMPDIR"' EXIT
34
+
35
+ CFG="$TMPDIR/config.yml"
36
+ cat >"$CFG" <<'YAML'
37
+ paginate: true
38
+ YAML
39
+
40
+ CFG_OUT=$(APEX_PAGER=cat "$APEX_BIN" --meta-file "$CFG" -t terminal <<< "$DOC")
41
+
42
+ if [[ "$BASE_OUT" != "$CFG_OUT" ]]; then
43
+ echo "paginate_cli_test: paginate: true output differs from baseline when using APEX_PAGER=cat" >&2
44
+ exit 1
45
+ fi
46
+
47
+ echo "paginate_cli_test: paginate: true config matches baseline output with APEX_PAGER=cat."
48
+
49
+ echo
50
+ echo "== Testing that --paginate is ignored for non-terminal formats =="
51
+
52
+ HTML_BASE=$("$APEX_BIN" -t html <<< "$DOC")
53
+ HTML_PAGED=$(APEX_PAGER=cat "$APEX_BIN" -t html --paginate <<< "$DOC")
54
+
55
+ if [[ "$HTML_BASE" != "$HTML_PAGED" ]]; then
56
+ echo "paginate_cli_test: -p should be a no-op for -t html" >&2
57
+ exit 1
58
+ fi
59
+
60
+ echo "paginate_cli_test: -p ignored for non-terminal formats as expected."
61
+
62
+ echo
63
+ echo "All paginate CLI tests passed."
64
+
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Sanity check that -t terminal --width N wraps output (CLI applies wrap after render).
3
+ set -euo pipefail
4
+
5
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ APEX_BIN="${APEX_BIN:-$ROOT/build/apex}"
7
+
8
+ if [[ ! -x "$APEX_BIN" ]]; then
9
+ echo "Error: $APEX_BIN not found or not executable. Set APEX_BIN or build first." >&2
10
+ exit 1
11
+ fi
12
+
13
+ # Long line without ANSI: with --width 10 we expect more than one line
14
+ LONG="this is a long line of plain text that should wrap"
15
+ OUT=$("$APEX_BIN" -t terminal --width 10 <<< "$LONG")
16
+ LINES=$(echo "$OUT" | wc -l | tr -d ' ')
17
+ if [[ "$LINES" -lt 2 ]]; then
18
+ echo "Expected --width 10 to wrap output into multiple lines, got $LINES line(s)" >&2
19
+ exit 1
20
+ fi
21
+
22
+ # With --width 80 the same line may stay on one line (or wrap less)
23
+ OUT2=$("$APEX_BIN" -t terminal --width 80 <<< "$LONG")
24
+ if [[ -z "$OUT2" ]]; then
25
+ echo "Expected non-empty output with --width 80" >&2
26
+ exit 1
27
+ fi
28
+
29
+ echo "terminal_width_test: --width wrapping OK"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ cd "$ROOT"
7
+
8
+ echo "=== Swift debug build ==="
9
+ swift build
10
+
11
+ echo "=== Swift release build ==="
12
+ swift build -c release
13
+
14
+ echo "Swift package OK."
@@ -0,0 +1,189 @@
1
+ //
2
+ // Created by Sbarex on 10/02/26.
3
+ //
4
+
5
+ #include "test_helpers.h"
6
+ #include "apex/apex.h"
7
+ #include <string.h>
8
+
9
+ /* cmark-gfm headers */
10
+ #include "cmark-gfm.h"
11
+ #include "cmark-gfm-extension_api.h"
12
+ #include "registry.h"
13
+ #include <string.h>
14
+
15
+ #include "render.h"
16
+ #include "parser.h"
17
+
18
+ #define DELIMITER ';'
19
+ #define DELIMITER_STR ";"
20
+
21
+ cmark_node_type CMARK_NODE_TEST;
22
+
23
+
24
+ // Match function: search the pattern ;;...;;
25
+ static cmark_node *match(__attribute__((unused)) cmark_syntax_extension *ext,
26
+ cmark_parser *parser,
27
+ __attribute__((unused)) cmark_node *parent,
28
+ unsigned char character,
29
+ cmark_inline_parser *inline_parser) {
30
+ cmark_node *res = NULL;
31
+ int left_flanking, right_flanking, punct_before, punct_after;
32
+ char buffer[101] = {0};
33
+
34
+ if (character != DELIMITER) {
35
+ return NULL;
36
+ }
37
+
38
+ int delims = cmark_inline_parser_scan_delimiters(
39
+ inline_parser, sizeof(buffer) - 1, DELIMITER,
40
+ &left_flanking,
41
+ &right_flanking, &punct_before, &punct_after);
42
+
43
+ memset(buffer, DELIMITER, delims);
44
+ buffer[delims] = 0;
45
+
46
+ res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
47
+ cmark_node_set_literal(res, buffer);
48
+ res->start_line = res->end_line = cmark_inline_parser_get_line(inline_parser);
49
+ res->start_column = cmark_inline_parser_get_column(inline_parser) - delims;
50
+
51
+ if ((left_flanking || right_flanking) && delims == 2) {
52
+ cmark_inline_parser_push_delimiter(inline_parser, character, left_flanking,
53
+ right_flanking, res);
54
+ }
55
+
56
+ return res;
57
+ }
58
+
59
+ static delimiter *insert(cmark_syntax_extension *self, __attribute__((unused)) cmark_parser *parser,
60
+ cmark_inline_parser *inline_parser, delimiter *opener,
61
+ delimiter *closer) {
62
+ cmark_node *tmp, *next;
63
+ delimiter *delim, *tmp_delim;
64
+ delimiter *res = closer->next;
65
+
66
+ cmark_node *node = opener->inl_text;
67
+
68
+ if (opener->inl_text->as.literal.len != closer->inl_text->as.literal.len)
69
+ goto done;
70
+
71
+ if (!cmark_node_set_type(node, CMARK_NODE_TEST))
72
+ goto done;
73
+
74
+ cmark_node_set_syntax_extension(node, self);
75
+
76
+ tmp = cmark_node_next(opener->inl_text);
77
+
78
+ while (tmp) {
79
+ if (tmp == closer->inl_text)
80
+ break;
81
+ next = cmark_node_next(tmp);
82
+ cmark_node_append_child(node, tmp);
83
+ tmp = next;
84
+ }
85
+
86
+ node->end_column = closer->inl_text->start_column + closer->inl_text->as.literal.len - 1;
87
+ cmark_node_free(closer->inl_text);
88
+
89
+ done:
90
+ delim = closer;
91
+ while (delim != NULL && delim != opener) {
92
+ tmp_delim = delim->previous;
93
+ cmark_inline_parser_remove_delimiter(inline_parser, delim);
94
+ delim = tmp_delim;
95
+ }
96
+
97
+ cmark_inline_parser_remove_delimiter(inline_parser, opener);
98
+
99
+ return res;
100
+ }
101
+
102
+ // Renderer HTML
103
+ static void html_render(__attribute__((unused)) cmark_syntax_extension *extension,
104
+ __attribute__((unused)) cmark_html_renderer *renderer,
105
+ __attribute__((unused)) cmark_node *node,
106
+ cmark_event_type ev_type,
107
+ __attribute__((unused)) int options) {
108
+ const bool entering = ev_type == CMARK_EVENT_ENTER;
109
+ if (entering) {
110
+ cmark_strbuf_puts(renderer->html, "<div class=\"custom\">");
111
+ } else {
112
+ cmark_strbuf_puts(renderer->html, "</div>");
113
+ }
114
+ }
115
+
116
+ // Function to get the namo the node
117
+ static const char *get_type_string(__attribute__((unused)) cmark_syntax_extension *ext, cmark_node *node) {
118
+ return node->type == CMARK_NODE_TEST ? "my_test" : "<unknown>";
119
+ }
120
+
121
+ static int can_contain(__attribute__((unused)) cmark_syntax_extension *ext, cmark_node *node,
122
+ cmark_node_type child_type) {
123
+ if (node->type != CMARK_NODE_TEST)
124
+ return false;
125
+
126
+ return CMARK_NODE_TYPE_INLINE_P(child_type);
127
+ }
128
+
129
+ cmark_syntax_extension *create_test_extension(void) {
130
+ cmark_syntax_extension *ext = cmark_syntax_extension_new("my_test");
131
+ cmark_llist *special_chars = NULL;
132
+
133
+ cmark_syntax_extension_set_get_type_string_func(ext, get_type_string);
134
+ cmark_syntax_extension_set_can_contain_func(ext, can_contain);
135
+ cmark_syntax_extension_set_html_render_func(ext, html_render);
136
+ CMARK_NODE_TEST = cmark_syntax_extension_add_node(1);
137
+
138
+ cmark_syntax_extension_set_match_inline_func(ext, match);
139
+ cmark_syntax_extension_set_inline_from_delim_func(ext, insert);
140
+
141
+ cmark_mem *mem = cmark_get_default_mem_allocator();
142
+ special_chars = cmark_llist_append(mem, special_chars, (void *)DELIMITER);
143
+ cmark_syntax_extension_set_special_inline_chars(ext, special_chars);
144
+
145
+ cmark_syntax_extension_set_emphasis(ext, 1);
146
+
147
+ return ext;
148
+ }
149
+
150
+ static int register_extra_extensions(cmark_plugin *plugin) {
151
+ cmark_plugin_register_syntax_extension(plugin, create_test_extension());
152
+ return 1;
153
+ }
154
+
155
+ static void my_cmark_init_callback(struct cmark_parser *parser, __attribute__((unused)) const apex_options *options, __attribute__((unused)) int cmark_opts, __attribute__((unused)) void *user_data) {
156
+ test_result(true, "Custom cmark init callback called");
157
+ cmark_register_plugin(register_extra_extensions);
158
+
159
+ cmark_syntax_extension *ext = cmark_find_syntax_extension("my_test");
160
+ if (ext) {
161
+ cmark_parser_attach_syntax_extension(parser, ext);
162
+ test_result(true, "Custom cmark extension named 'my_test' registered");
163
+ } else {
164
+ test_result(false, "Unable to find custom cmark extension named 'my_test'!");
165
+ }
166
+ }
167
+
168
+ static void my_cmark_done_callback(__attribute__((unused)) struct cmark_parser *parser, __attribute__((unused)) const apex_options *options, __attribute__((unused)) int cmark_opts, __attribute__((unused)) void *user_data) {
169
+ test_result(true, "Custom cmark done callback called");
170
+ }
171
+
172
+ void test_cmark_callback(void) {
173
+ int suite_failures = suite_start();
174
+ print_suite_title("Cmark Callbacks Tests", false, true);
175
+
176
+ apex_options opts = apex_options_default();
177
+ opts.cmark_init = my_cmark_init_callback;
178
+ opts.cmark_done = my_cmark_done_callback;
179
+
180
+ char *html;
181
+
182
+ const char *s = "#Custom cmark extension test\n\nHi " DELIMITER_STR DELIMITER_STR "this text must be highlighted" DELIMITER_STR DELIMITER_STR "!";
183
+ html = apex_markdown_to_html(s, strlen(s), &opts);
184
+ assert_contains(html, "<div class=\"custom\">", "Custom cmark extension");
185
+ apex_free_string(html);
186
+
187
+ bool had_failures = suite_end(suite_failures);
188
+ print_suite_title("Cmark Callbacks Tests", had_failures, false);
189
+ }