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
@@ -5,6 +5,8 @@
5
5
  #include "test_helpers.h"
6
6
  #include "apex/apex.h"
7
7
  #include "../src/extensions/advanced_footnotes.h"
8
+ #include <stdio.h>
9
+ #include <stdlib.h>
8
10
  #include <string.h>
9
11
 
10
12
  void test_math(void) {
@@ -174,6 +176,44 @@ void test_processor_modes(void) {
174
176
  print_suite_title("Processor Modes Tests", had_failures, false);
175
177
  }
176
178
 
179
+ /**
180
+ * Test cmark_init callback for custom extension registration
181
+ */
182
+ static int cmark_init_callback_invoked = 0;
183
+
184
+ static void test_cmark_init_cb(struct cmark_parser *parser,
185
+ const struct apex_options *opts,
186
+ int cmark_opts,
187
+ __attribute__((unused)) void *user_data) {
188
+ (void)parser;
189
+ (void)opts;
190
+ (void)cmark_opts;
191
+ cmark_init_callback_invoked = 1;
192
+ }
193
+
194
+ void test_cmark_init_callback(void) {
195
+ int suite_failures = suite_start();
196
+ print_suite_title("cmark_init callback Tests", false, true);
197
+
198
+ cmark_init_callback_invoked = 0;
199
+ apex_options opts = apex_options_default();
200
+ opts.cmark_init = test_cmark_init_cb;
201
+ char *html = apex_markdown_to_html("# Hi", 4, &opts);
202
+ test_result(cmark_init_callback_invoked == 1, "cmark_init callback was invoked");
203
+ assert_contains(html, "<h1", "Basic parsing still works with callback");
204
+ assert_contains(html, "Hi</h1>", "Header content preserved");
205
+ apex_free_string(html);
206
+
207
+ /* NULL callback: conversion works normally */
208
+ opts.cmark_init = NULL;
209
+ html = apex_markdown_to_html("**bold**", 8, &opts);
210
+ assert_contains(html, "<strong>bold</strong>", "Parsing works with NULL callback");
211
+ apex_free_string(html);
212
+
213
+ bool had_failures = suite_end(suite_failures);
214
+ print_suite_title("cmark_init callback Tests", had_failures, false);
215
+ }
216
+
177
217
  /**
178
218
  * Test MultiMarkdown-style image attributes (inline and reference)
179
219
  */
@@ -327,6 +367,161 @@ void test_multimarkdown_image_attributes(void) {
327
367
  apex_free_string(html);
328
368
  }
329
369
 
370
+ /* webp attribute: ![alt](url webp) emits <picture> with webp source */
371
+ {
372
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
373
+ const char *webp_md = "![Hero](img/hero.png webp)";
374
+ char *html = apex_markdown_to_html(webp_md, strlen(webp_md), &opts);
375
+ assert_contains(html, "<picture>", "webp: picture element");
376
+ assert_contains(html, "type=\"image/webp\"", "webp: source type");
377
+ assert_contains(html, "img/hero.webp 1x", "webp: srcset");
378
+ assert_contains(html, "<img src=\"img/hero.png\"", "webp: img fallback");
379
+ assert_contains(html, "</picture>", "webp: picture close");
380
+ apex_free_string(html);
381
+ }
382
+
383
+ /* avif attribute: ![alt](url avif) emits <picture> with avif source */
384
+ {
385
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
386
+ const char *avif_md = "![Hero](img/hero.png avif)";
387
+ char *html = apex_markdown_to_html(avif_md, strlen(avif_md), &opts);
388
+ assert_contains(html, "<picture>", "avif: picture element");
389
+ assert_contains(html, "type=\"image/avif\"", "avif: source type");
390
+ assert_contains(html, "img/hero.avif 1x", "avif: srcset");
391
+ assert_contains(html, "<img src=\"img/hero.png\"", "avif: img fallback");
392
+ apex_free_string(html);
393
+ }
394
+
395
+ /* webp + @2x: srcset includes 1x and 2x for webp */
396
+ {
397
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
398
+ const char *webp_2x_md = "![Hero](img/hero.png webp @2x)";
399
+ char *html = apex_markdown_to_html(webp_2x_md, strlen(webp_2x_md), &opts);
400
+ assert_contains(html, "img/hero.webp 1x, img/hero@2x.webp 2x", "webp @2x: srcset");
401
+ apex_free_string(html);
402
+ }
403
+
404
+ /* Video URL: ![alt](video.mp4) emits <video> */
405
+ {
406
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
407
+ const char *video_md = "![Demo](media/demo.mp4)";
408
+ char *html = apex_markdown_to_html(video_md, strlen(video_md), &opts);
409
+ assert_contains(html, "<video", "video: video element");
410
+ assert_contains(html, "media/demo.mp4", "video: src");
411
+ assert_contains(html, "</video>", "video: close");
412
+ assert_not_contains(html, "<img", "video: no img");
413
+ apex_free_string(html);
414
+ }
415
+
416
+ /* Video with webm attribute: adds webm source */
417
+ {
418
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
419
+ const char *video_webm_md = "![Demo](media/demo.mp4 webm)";
420
+ char *html = apex_markdown_to_html(video_webm_md, strlen(video_webm_md), &opts);
421
+ assert_contains(html, "<source src=\"media/demo.webm\" type=\"video/webm\">", "video webm: source");
422
+ assert_contains(html, "<source src=\"media/demo.mp4\"", "video webm: primary fallback");
423
+ apex_free_string(html);
424
+ }
425
+
426
+ /* Fixture-based tests: media_formats_test.md */
427
+ {
428
+ const char *fixture_path = "tests/fixtures/images/media_formats_test.md";
429
+ FILE *fp = fopen(fixture_path, "rb");
430
+ if (fp) {
431
+ fseek(fp, 0, SEEK_END);
432
+ long sz = ftell(fp);
433
+ fseek(fp, 0, SEEK_SET);
434
+ char *src = malloc(sz + 1);
435
+ if (src) {
436
+ size_t len = fread(src, 1, (size_t)sz, fp);
437
+ src[len] = '\0';
438
+ fclose(fp);
439
+
440
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
441
+ char *html = apex_markdown_to_html(src, len, &opts);
442
+
443
+ /* Images with webp/avif: picture elements */
444
+ assert_contains(html, "<picture>", "fixture: picture elements");
445
+ assert_contains(html, "type=\"image/webp\"", "fixture: webp source type");
446
+ assert_contains(html, "type=\"image/avif\"", "fixture: avif source type");
447
+ assert_contains(html, "img/hero.webp 1x", "fixture: webp srcset");
448
+ assert_contains(html, "img/hero.avif 1x", "fixture: avif srcset");
449
+ assert_contains(html, "img/hero@2x.webp 2x", "fixture: webp @2x");
450
+ assert_contains(html, "img/hero@2x.avif 2x", "fixture: avif @2x");
451
+
452
+ /* Videos: video elements */
453
+ assert_contains(html, "<video", "fixture: video elements");
454
+ assert_contains(html, "media/demo.mp4", "fixture: mp4 video");
455
+ assert_contains(html, "assets/trailer.mov", "fixture: mov video");
456
+ assert_contains(html, "assets/sample.m4v", "fixture: m4v video");
457
+
458
+ /* Video with format alternatives */
459
+ assert_contains(html, "media/demo.webm", "fixture: video webm source");
460
+ assert_contains(html, "media/intro.ogg", "fixture: video ogg source");
461
+ assert_contains(html, "media/clip.mp4", "fixture: webm with mp4 fallback");
462
+
463
+ /* Auto attribute (marker present; expansion requires base_directory) */
464
+ assert_contains(html, "data-apex-replace-auto=1", "fixture: auto marker");
465
+
466
+ apex_free_string(html);
467
+ free(src);
468
+ } else {
469
+ fclose(fp);
470
+ test_result(false, "fixture: malloc failed for media_formats_test.md");
471
+ }
472
+ } else {
473
+ test_result(false, "fixture: could not open media_formats_test.md");
474
+ }
475
+ }
476
+
477
+ /* auto attribute: emits data-apex-replace-auto (expansion requires base_directory) */
478
+ {
479
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
480
+ const char *auto_md = "![Hero](img/hero.png auto)";
481
+ char *html = apex_markdown_to_html(auto_md, strlen(auto_md), &opts);
482
+ assert_contains(html, "data-apex-replace-auto=1", "auto: emits replace marker");
483
+ apex_free_string(html);
484
+ }
485
+
486
+ /* auto with base_directory: discovers img set (jpg, webp, avif, 2x variants), emits picture */
487
+ {
488
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
489
+ opts.base_directory = "tests/fixtures/images";
490
+ const char *auto_md = "![Profile menu](img/app-pass-1-profile-menu.jpg auto)";
491
+ char *html = apex_markdown_to_html(auto_md, strlen(auto_md), &opts);
492
+ assert_contains(html, "<picture>", "auto+base: picture element");
493
+ assert_contains(html, "type=\"image/avif\"", "auto+base: avif source");
494
+ assert_contains(html, "type=\"image/webp\"", "auto+base: webp source");
495
+ assert_contains(html, "app-pass-1-profile-menu.avif 1x", "auto+base: avif 1x");
496
+ assert_contains(html, "app-pass-1-profile-menu@2x.avif 2x", "auto+base: avif 2x");
497
+ assert_contains(html, "app-pass-1-profile-menu.webp 1x", "auto+base: webp 1x");
498
+ assert_contains(html, "app-pass-1-profile-menu@2x.webp 2x", "auto+base: webp 2x");
499
+ assert_contains(html, "<img src=\"img/app-pass-1-profile-menu.jpg\"", "auto+base: img fallback");
500
+ apex_free_string(html);
501
+ }
502
+
503
+ /* * extension: equivalent to auto, discovers formats from base filename */
504
+ {
505
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
506
+ opts.base_directory = "tests/fixtures/images";
507
+ const char *wildcard_md = "![Profile menu](img/app-pass-1-profile-menu.*)";
508
+ char *html = apex_markdown_to_html(wildcard_md, strlen(wildcard_md), &opts);
509
+ assert_contains(html, "<picture>", "wildcard: picture element");
510
+ assert_contains(html, "type=\"image/avif\"", "wildcard: avif source");
511
+ assert_contains(html, "type=\"image/webp\"", "wildcard: webp source");
512
+ assert_contains(html, "app-pass-1-profile-menu.jpg", "wildcard: jpg fallback");
513
+ apex_free_string(html);
514
+ }
515
+
516
+ /* * extension: emits data-apex-replace-auto marker when no base_directory */
517
+ {
518
+ apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
519
+ const char *wildcard_md = "![Hero](img/hero.*)";
520
+ char *html = apex_markdown_to_html(wildcard_md, strlen(wildcard_md), &opts);
521
+ assert_contains(html, "data-apex-replace-auto=1", "wildcard: emits replace marker");
522
+ apex_free_string(html);
523
+ }
524
+
330
525
  bool had_failures = suite_end(suite_failures);
331
526
  print_suite_title("MultiMarkdown Image Attribute Tests", had_failures, false);
332
527
  }
@@ -383,12 +578,49 @@ void test_file_includes(void) {
383
578
  assert_contains(html, "New York", "CSV cell content");
384
579
  apex_free_string(html);
385
580
 
581
+ /* Test Marked CSV include with embedded shorthand delimiter override */
582
+ const char *marked_csv_embedded_short = "Data:\n\n<<[data-semi.csv{;}]\n\nEnd";
583
+ html = apex_markdown_to_html(marked_csv_embedded_short, strlen(marked_csv_embedded_short), &opts);
584
+ assert_contains(html, "<table>", "Marked CSV include with embedded {;} converts to table");
585
+ assert_contains(html, "San Francisco", "Marked CSV include with embedded {;} parses semicolon-separated values");
586
+ assert_not_contains(html, "{;}", "Marked CSV include consumes embedded {;} delimiter token");
587
+ apex_free_string(html);
588
+
589
+ /* Test Marked CSV include with embedded verbose delimiter override */
590
+ const char *marked_csv_embedded_verbose = "Data:\n\n<<[data-semi.csv{delimiter=;}]\n\nEnd";
591
+ html = apex_markdown_to_html(marked_csv_embedded_verbose, strlen(marked_csv_embedded_verbose), &opts);
592
+ assert_contains(html, "<table>", "Marked CSV include with embedded {delimiter=;} converts to table");
593
+ assert_contains(html, "Alice", "Marked CSV include with embedded {delimiter=;} parses semicolon-separated values");
594
+ assert_not_contains(html, "{delimiter=;}", "Marked CSV include consumes embedded verbose delimiter token");
595
+ apex_free_string(html);
596
+
386
597
  /* Test TSV to table conversion */
387
598
  html = apex_markdown_to_html("{{data.tsv}}", 12, &opts);
388
599
  assert_contains(html, "<table>", "TSV converts to table");
389
600
  assert_contains(html, "Widget", "TSV data in table");
390
601
  apex_free_string(html);
391
602
 
603
+ /* Test MMD CSV transclusion with embedded shorthand delimiter override */
604
+ const char *mmd_csv_embedded_short = "{{data-semi.csv{;}}}";
605
+ html = apex_markdown_to_html(mmd_csv_embedded_short, strlen(mmd_csv_embedded_short), &opts);
606
+ assert_contains(html, "<table>", "MMD CSV transclusion with embedded {;} converts to table");
607
+ assert_contains(html, "San Francisco", "MMD CSV transclusion with embedded {;} parses semicolon-separated values");
608
+ assert_not_contains(html, "{;}", "MMD CSV transclusion consumes embedded {;} delimiter token");
609
+ apex_free_string(html);
610
+
611
+ /* Test MMD CSV transclusion with embedded verbose delimiter override */
612
+ const char *mmd_csv_embedded_verbose = "{{data-semi.csv{delimiter=;}}}";
613
+ html = apex_markdown_to_html(mmd_csv_embedded_verbose, strlen(mmd_csv_embedded_verbose), &opts);
614
+ assert_contains(html, "<table>", "MMD CSV transclusion with embedded {delimiter=;} converts to table");
615
+ assert_contains(html, "Alice", "MMD CSV transclusion with embedded {delimiter=;} parses semicolon-separated values");
616
+ assert_not_contains(html, "{delimiter=;}", "MMD CSV transclusion consumes embedded verbose delimiter token");
617
+ apex_free_string(html);
618
+
619
+ /* Test percent-encoded path in include */
620
+ html = apex_markdown_to_html("<<[with%20space.txt]", 21, &opts);
621
+ assert_contains(html, "Percent-decoded", "Percent-encoded path (with%20space.txt) resolves to file with space");
622
+ apex_free_string(html);
623
+
392
624
  /* Test iA Writer image include */
393
625
  html = apex_markdown_to_html("/image.png", 10, &opts);
394
626
  assert_contains(html, "<img", "iA Writer image include");
@@ -401,6 +633,22 @@ void test_file_includes(void) {
401
633
  assert_contains(html, "def hello", "Code included");
402
634
  apex_free_string(html);
403
635
 
636
+ /* Test iA Writer CSV include with verbose delimiter keyword */
637
+ const char *ia_csv_delim_override_doc = "/data-semi.csv {delimiter=;}";
638
+ html = apex_markdown_to_html(ia_csv_delim_override_doc, strlen(ia_csv_delim_override_doc), &opts);
639
+ assert_contains(html, "<table>", "iA Writer CSV include with {delimiter=;} converts to table");
640
+ assert_contains(html, "Alice", "iA Writer CSV include parses semicolon-separated values");
641
+ assert_not_contains(html, "{delimiter=;}", "iA Writer include consumes verbose delimiter token");
642
+ apex_free_string(html);
643
+
644
+ /* Test iA Writer CSV include with embedded shorthand delimiter (no whitespace) */
645
+ const char *ia_csv_embedded_short = "/data-semi.csv{;}";
646
+ html = apex_markdown_to_html(ia_csv_embedded_short, strlen(ia_csv_embedded_short), &opts);
647
+ assert_contains(html, "<table>", "iA Writer CSV include with embedded {;} converts to table");
648
+ assert_contains(html, "San Francisco", "iA Writer CSV include with embedded {;} parses semicolon-separated values");
649
+ assert_not_contains(html, "{;}", "iA Writer include consumes embedded shorthand delimiter token");
650
+ apex_free_string(html);
651
+
404
652
  /* Test glob wildcard: *.md (should resolve to one of the .md fixtures) */
405
653
  html = apex_markdown_to_html("{{*.md}}", 10, &opts);
406
654
  if (strstr(html, "Included Content") != NULL ||
@@ -629,6 +877,56 @@ void test_definition_lists(void) {
629
877
  assert_contains(html, "<dd>definition 2</dd>", "Second definition");
630
878
  apex_free_string(html);
631
879
 
880
+ /* Test all four definition list formats */
881
+ html = apex_markdown_to_html("term\n: definition", 17, &opts);
882
+ assert_contains(html, "<dt>term</dt>", "Kramdown format: term + : definition");
883
+ assert_contains(html, "<dd>definition</dd>", "Kramdown format dd");
884
+ apex_free_string(html);
885
+
886
+ html = apex_markdown_to_html("term\n:: definition", 18, &opts);
887
+ assert_contains(html, "<dt>term</dt>", "Kramdown format: term + :: definition");
888
+ assert_contains(html, "<dd>definition</dd>", "Kramdown :: format dd");
889
+ apex_free_string(html);
890
+
891
+ html = apex_markdown_to_html("term::definition", 16, &opts);
892
+ assert_contains(html, "<dt>term</dt>", "One-line format: term::definition");
893
+ assert_contains(html, "<dd>definition</dd>", "One-line format dd");
894
+ apex_free_string(html);
895
+
896
+ html = apex_markdown_to_html("term :: definition", 18, &opts);
897
+ assert_contains(html, "<dt>term</dt>", "One-line format: term :: definition");
898
+ assert_contains(html, "<dd>definition</dd>", "One-line format with spaces dd");
899
+ apex_free_string(html);
900
+
901
+ /* One-line definition NOT converted inside inline code span */
902
+ html = apex_markdown_to_html("Use `term::definition` for syntax", 32, &opts);
903
+ assert_contains(html, "<code>term::definition</code>", "One-line def preserved in inline code");
904
+ assert_not_contains(html, "<dt>term</dt>", "No definition list from content inside backticks");
905
+ apex_free_string(html);
906
+
907
+ /* One-line definition NOT converted inside fenced code block */
908
+ const char *fenced_def = "```\nfoo::bar\nterm::definition\n```";
909
+ html = apex_markdown_to_html(fenced_def, strlen(fenced_def), &opts);
910
+ assert_contains(html, "foo::bar", "One-line def in fenced block preserved");
911
+ assert_contains(html, "term::definition", "Second one-line def in fenced block preserved");
912
+ assert_not_contains(html, "<dt>foo</dt>", "No definition list from fenced code content");
913
+ apex_free_string(html);
914
+
915
+ /* One-line definition NOT converted inside indented code block */
916
+ const char *indented_def = " key::value\n term::definition";
917
+ html = apex_markdown_to_html(indented_def, strlen(indented_def), &opts);
918
+ assert_contains(html, "key::value", "One-line def in indented block preserved");
919
+ assert_contains(html, "term::definition", "Second one-line def in indented block preserved");
920
+ assert_not_contains(html, "<dt>key</dt>", "No definition list from indented code content");
921
+ apex_free_string(html);
922
+
923
+ /* Kramdown : definition NOT converted inside multi-line inline code span */
924
+ const char *multiline_code = "`term::works\n :more:`";
925
+ html = apex_markdown_to_html(multiline_code, strlen(multiline_code), &opts);
926
+ assert_contains(html, "term::works", "One-line def in multi-line inline code preserved");
927
+ assert_not_contains(html, "<dt>term</dt>", "No definition list from multi-line inline code");
928
+ apex_free_string(html);
929
+
632
930
  bool had_failures = suite_end(suite_failures);
633
931
  print_suite_title("Definition Lists Tests", had_failures, false);
634
932
  }
@@ -1201,6 +1499,13 @@ void test_abbreviations(void) {
1201
1499
  assert_contains(html, "New Style", "New syntax in mixed");
1202
1500
  apex_free_string(html);
1203
1501
 
1502
+ /* Test inline and reference-style in same document (both must be wrapped) */
1503
+ const char *inline_and_ref = "This is HTML. And more [>(ABBR) abbreviation syntax].\n\n[>HTML]: Hypertext Markup Language";
1504
+ html = apex_markdown_to_html(inline_and_ref, strlen(inline_and_ref), &opts);
1505
+ assert_contains(html, "<abbr title=\"Hypertext Markup Language\">HTML</abbr>", "Reference abbr when mixed");
1506
+ assert_contains(html, "<abbr title=\"abbreviation syntax\">ABBR</abbr>", "Inline abbr when mixed");
1507
+ apex_free_string(html);
1508
+
1204
1509
  bool had_failures = suite_end(suite_failures);
1205
1510
  print_suite_title("Abbreviations Tests", had_failures, false);
1206
1511
  }
@@ -1352,6 +1657,26 @@ void test_emoji(void) {
1352
1657
  assert_contains(html, "👍", "Plus one emoji");
1353
1658
  apex_free_string(html);
1354
1659
 
1660
+ /* Emoji NOT converted inside inline code span */
1661
+ html = apex_markdown_to_html("Use `:smile:` for emoji syntax", 30, &opts);
1662
+ assert_contains(html, "<code>:smile:</code>", "Emoji preserved as literal in inline code");
1663
+ assert_not_contains(html, "😄", "Emoji character not in code span output");
1664
+ apex_free_string(html);
1665
+
1666
+ /* Emoji NOT converted inside fenced code block */
1667
+ const char *fenced_emoji = "```\n:smile:\n:rocket:\n```";
1668
+ html = apex_markdown_to_html(fenced_emoji, strlen(fenced_emoji), &opts);
1669
+ assert_contains(html, ":smile:", "Emoji pattern preserved in fenced block");
1670
+ assert_contains(html, ":rocket:", "Second emoji preserved in fenced block");
1671
+ apex_free_string(html);
1672
+
1673
+ /* Emoji NOT converted inside indented code block */
1674
+ const char *indented_emoji = " :smile:\n :rocket:";
1675
+ html = apex_markdown_to_html(indented_emoji, strlen(indented_emoji), &opts);
1676
+ assert_contains(html, ":smile:", "Emoji pattern preserved in indented block");
1677
+ assert_contains(html, ":rocket:", "Second emoji preserved in indented block");
1678
+ apex_free_string(html);
1679
+
1355
1680
  bool had_failures = suite_end(suite_failures);
1356
1681
  print_suite_title("Emoji Tests", had_failures, false);
1357
1682
  }
@@ -1802,6 +2127,55 @@ void test_sup_sub(void) {
1802
2127
  test_result(false, "Subscript incorrectly processed in critic markup");
1803
2128
  }
1804
2129
  apex_free_string(html);
2130
+ opts.enable_critic_markup = false;
2131
+
2132
+ /* ===== CODE BLOCKS: extended syntax not processed ===== */
2133
+
2134
+ /* Indented code block: ^ ~ ~~ == must appear literally, not as sup/sub/underline/strikethrough/highlight */
2135
+ const char *indented_with_extended = "Normal text x^2 here.\n\n code with ~subscript~ and ^caret^\n and ~~strikethrough~~ and ==highlight==\n\nBack to normal.";
2136
+ html = apex_markdown_to_html(indented_with_extended, strlen(indented_with_extended), &opts);
2137
+ assert_contains(html, "<sup>2</sup>", "Superscript processed in normal text");
2138
+ assert_contains(html, "~subscript~", "Subscript not processed in indented code block");
2139
+ assert_contains(html, "^caret^", "Caret not processed as superscript in indented code block");
2140
+ assert_not_contains(html, "<sub>subscript</sub>", "No subscript tag in indented code block");
2141
+ assert_contains(html, "~~strikethrough~~", "Strikethrough not processed in indented code block");
2142
+ assert_contains(html, "==highlight==", "Highlight not processed in indented code block");
2143
+ assert_not_contains(html, "<mark>highlight</mark>", "No mark tag in indented code block");
2144
+ apex_free_string(html);
2145
+
2146
+ /* Fenced code block: same checks */
2147
+ const char *fenced_with_extended = "Normal x^2.\n\n```\ncode with ~subscript~ and ^caret^\nand ~~strikethrough~~ and ==highlight==\n```\n\nBack.";
2148
+ html = apex_markdown_to_html(fenced_with_extended, strlen(fenced_with_extended), &opts);
2149
+ assert_contains(html, "<sup>2</sup>", "Superscript processed in normal text");
2150
+ assert_contains(html, "~subscript~", "Subscript not processed in fenced code block");
2151
+ assert_contains(html, "^caret^", "Caret not processed in fenced code block");
2152
+ assert_not_contains(html, "<sub>subscript</sub>", "No subscript tag in fenced code block");
2153
+ assert_contains(html, "~~strikethrough~~", "Strikethrough not processed in fenced code block");
2154
+ assert_contains(html, "==highlight==", "Highlight not processed in fenced code block");
2155
+ apex_free_string(html);
2156
+
2157
+ /* Inline code: ^ and ~ must not be processed */
2158
+ html = apex_markdown_to_html("Use `x^2` and `H~2~O` in code.", 32, &opts);
2159
+ assert_contains(html, "x^2", "Caret preserved in inline code");
2160
+ assert_contains(html, "H~2~O", "Tildes preserved in inline code");
2161
+ assert_not_contains(html, "<sup>2</sup>", "No superscript in inline code");
2162
+ assert_not_contains(html, "<sub>2</sub>", "No subscript in inline code");
2163
+ apex_free_string(html);
2164
+
2165
+ /* Nested list with 4+ spaces: sup/sub and highlight should be processed (list line, not code block) */
2166
+ const char *list_with_extended = "- Outer item\n - Nested with x^2 and H~2~O\n - And ==highlight== here";
2167
+ html = apex_markdown_to_html(list_with_extended, strlen(list_with_extended), &opts);
2168
+ assert_contains(html, "<sup>2</sup>", "Superscript processed in nested list line");
2169
+ assert_contains(html, "<sub>2</sub>", "Subscript processed in nested list line");
2170
+ assert_contains(html, "<mark>highlight</mark>", "Highlight processed in nested list line");
2171
+ apex_free_string(html);
2172
+
2173
+ /* Indented line that is real code (no list marker): still no processing */
2174
+ const char *real_indented_code = "Paragraph.\n\n actual code ~subscript~ here\n\nBack.";
2175
+ html = apex_markdown_to_html(real_indented_code, strlen(real_indented_code), &opts);
2176
+ assert_contains(html, "~subscript~", "Subscript not processed in real indented code block");
2177
+ assert_not_contains(html, "<sub>subscript</sub>", "No subscript tag in real indented code block");
2178
+ apex_free_string(html);
1805
2179
 
1806
2180
  bool had_failures = suite_end(suite_failures);
1807
2181
  print_suite_title("Superscript, Subscript, Underline, Delete, and Highlight Tests", had_failures, false);
@@ -183,6 +183,34 @@ void test_mmd_metadata_keys(void) {
183
183
  assert_contains(html, "</body>", "HTML Footer metadata before </body>");
184
184
  apex_free_string(html);
185
185
 
186
+ /* Test generic metadata tags in standalone HTML head */
187
+ opts.standalone = true;
188
+ const char *head_meta_doc =
189
+ "Title: This is the Title\n"
190
+ "Author: That would be me\n"
191
+ "Date: March 9, 2026\n\n"
192
+ "# Test";
193
+ html = apex_markdown_to_html(head_meta_doc, strlen(head_meta_doc), &opts);
194
+ assert_contains(html, "<meta name=\"Author\" content=\"That would be me\"/>", "Author metadata emitted as meta tag");
195
+ assert_contains(html, "<meta name=\"Date\" content=\"March 9, 2026\"/>", "Date metadata emitted as meta tag");
196
+ apex_free_string(html);
197
+
198
+ /* Test generic metadata tags in standalone HTML head (Unified mode) */
199
+ apex_options unified_head_opts = apex_options_for_mode(APEX_MODE_UNIFIED);
200
+ unified_head_opts.standalone = true;
201
+ html = apex_markdown_to_html(head_meta_doc, strlen(head_meta_doc), &unified_head_opts);
202
+ assert_contains(html, "<meta name=\"Author\" content=\"That would be me\"/>", "Unified: author metadata emitted as meta tag");
203
+ assert_contains(html, "<meta name=\"Date\" content=\"March 9, 2026\"/>", "Unified: date metadata emitted as meta tag");
204
+ apex_free_string(html);
205
+
206
+ /* Test generic metadata tags in standalone HTML head (Kramdown mode) */
207
+ apex_options kramdown_head_opts = apex_options_for_mode(APEX_MODE_KRAMDOWN);
208
+ kramdown_head_opts.standalone = true;
209
+ html = apex_markdown_to_html(head_meta_doc, strlen(head_meta_doc), &kramdown_head_opts);
210
+ assert_contains(html, "<meta name=\"Author\" content=\"That would be me\"/>", "Kramdown: author metadata emitted as meta tag");
211
+ assert_contains(html, "<meta name=\"Date\" content=\"March 9, 2026\"/>", "Kramdown: date metadata emitted as meta tag");
212
+ apex_free_string(html);
213
+
186
214
  /* Test normalized key matching (spaces removed, case-insensitive) */
187
215
  opts.standalone = false;
188
216
  opts.enable_smart_typography = true; /* Ensure smart typography is enabled */
@@ -200,6 +228,20 @@ void test_mmd_metadata_keys(void) {
200
228
  assert_contains(html, "&bdquo;", "Case-insensitive: QUOTES LANGUAGE works");
201
229
  apex_free_string(html);
202
230
 
231
+ /* MMD delimiter block compatibility: opening/closing with repeated chars */
232
+ opts.standalone = true;
233
+ const char *mmd_delimited_doc =
234
+ "----\n"
235
+ "Title: Delimited Title\n"
236
+ "Author: Delimited Author\n"
237
+ "......\n"
238
+ "\n"
239
+ "# Body";
240
+ html = apex_markdown_to_html(mmd_delimited_doc, strlen(mmd_delimited_doc), &opts);
241
+ assert_contains(html, "<meta name=\"Author\" content=\"Delimited Author\"/>", "MMD delimiter block: author parsed");
242
+ assert_not_contains(html, "......", "MMD delimiter block: dot closer not rendered in body");
243
+ apex_free_string(html);
244
+
203
245
  bool had_failures = suite_end(suite_failures);
204
246
  print_suite_title("MultiMarkdown Metadata Keys Tests", had_failures, false);
205
247
  }
@@ -747,6 +789,32 @@ void test_syntax_highlight_options(void) {
747
789
  apex_free_metadata(metadata);
748
790
  metadata = NULL;
749
791
 
792
+ /* Test code-highlight option: shiki (full name) */
793
+ opts = apex_options_default();
794
+ item = malloc(sizeof(apex_metadata_item));
795
+ item->key = strdup("code-highlight");
796
+ item->value = strdup("shiki");
797
+ item->next = NULL;
798
+ metadata = item;
799
+
800
+ apex_apply_metadata_to_options(metadata, &opts);
801
+ assert_option_string(opts.code_highlighter, "shiki", "code-highlight: shiki sets code_highlighter");
802
+ apex_free_metadata(metadata);
803
+ metadata = NULL;
804
+
805
+ /* Test code-highlight option: sh (abbreviation for shiki) */
806
+ opts = apex_options_default();
807
+ item = malloc(sizeof(apex_metadata_item));
808
+ item->key = strdup("code-highlight");
809
+ item->value = strdup("sh");
810
+ item->next = NULL;
811
+ metadata = item;
812
+
813
+ apex_apply_metadata_to_options(metadata, &opts);
814
+ assert_option_string(opts.code_highlighter, "shiki", "code-highlight: sh (abbreviation) sets shiki");
815
+ apex_free_metadata(metadata);
816
+ metadata = NULL;
817
+
750
818
  /* Test code-highlight option: false disables */
751
819
  opts = apex_options_default();
752
820
  opts.code_highlighter = "pygments"; /* Start with it enabled */