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.
- checksums.yaml +4 -4
- data/ext/apex_ext/apex_ext.c +6 -0
- data/ext/apex_ext/apex_src/AGENTS.md +41 -0
- data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
- data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
- data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
- data/ext/apex_ext/apex_src/Package.swift +9 -0
- data/ext/apex_ext/apex_src/README.md +31 -9
- data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
- data/ext/apex_ext/apex_src/VERSION +1 -1
- data/ext/apex_ext/apex_src/cli/main.c +1125 -13
- data/ext/apex_ext/apex_src/docs/index.md +459 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
- data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
- data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
- data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
- data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
- data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
- data/ext/apex_ext/apex_src/man/apex.1 +663 -620
- data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
- data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
- data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
- data/ext/apex_ext/apex_src/pages/index.md +459 -0
- data/ext/apex_ext/apex_src/src/_README.md +4 -4
- data/ext/apex_ext/apex_src/src/apex.c +702 -44
- data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
- data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
- data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
- data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
- data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
- data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
- data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
- data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
- data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
- data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
- data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
- data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
- data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
- data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
- data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
- data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
- data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
- data/ext/apex_ext/apex_src/tests/README.md +11 -5
- data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
- data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
- data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
- data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
- data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
- data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
- data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
- data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
- data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
- data/lib/apex/version.rb +1 -1
- metadata +32 -2
- data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +0 -456
|
@@ -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:  emits <picture> with webp source */
|
|
371
|
+
{
|
|
372
|
+
apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
|
|
373
|
+
const char *webp_md = "";
|
|
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:  emits <picture> with avif source */
|
|
384
|
+
{
|
|
385
|
+
apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
|
|
386
|
+
const char *avif_md = "";
|
|
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 = "";
|
|
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:  emits <video> */
|
|
405
|
+
{
|
|
406
|
+
apex_options opts = apex_options_for_mode(APEX_MODE_UNIFIED);
|
|
407
|
+
const char *video_md = "";
|
|
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 = "";
|
|
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 = "";
|
|
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 = "";
|
|
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 = "";
|
|
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 = "";
|
|
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, "„", "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 */
|