File Coverage

lib/Markdown/Simple.xs
Criterion Covered Total %
statement 369 421 87.6
branch 369 586 62.9
condition n/a
subroutine n/a
pod n/a
total 738 1007 73.2


line stmt bran cond sub pod time code
1             #include "EXTERN.h"
2             #include "perl.h"
3             #include "XSUB.h"
4              
5             typedef struct {
6             int enable_preprocess;
7             int enable_headers;
8             int enable_bold;
9             int enable_italic;
10             int enable_links;
11             int enable_images;
12             int enable_code;
13             int enable_tables;
14             int enable_tasklist;
15             int enable_fenced_code;
16             int enable_strikethrough;
17             int enable_ordered_lists;
18             int enable_unordered_lists;
19             } MarkdownOptions;
20              
21 81           static SV* markdown_to_html(const char* input, MarkdownOptions* opts) {
22 81           SV* out = newSVpv("", 0);
23 81           const char* p = input;
24              
25             /* Escape all non-ASCII UTF-8 characters (do not escape HTML) */
26 1416 100         while (*p) {
27 1335           unsigned char c = (unsigned char)*p;
28 1335 100         if (c >= 0x80) {
29             // Start of a UTF-8 multibyte sequence
30 9           int len = 1;
31 9 100         if ((c & 0xE0) == 0xC0) len = 2;
32 1 50         else if ((c & 0xF0) == 0xE0) len = 3;
33 1 50         else if ((c & 0xF8) == 0xF0) len = 4;
34 0           else { p++; continue; } // Invalid, skip
35              
36 9           UV codepoint = 0;
37 9 100         if (len == 2 && (p[1] & 0xC0) == 0x80) {
    50          
38 8           codepoint = ((p[0] & 0x1F) << 6) | (p[1] & 0x3F);
39 1 50         } else if (len == 3 && (p[1] & 0xC0) == 0x80 && (p[2] & 0xC0) == 0x80) {
    0          
    0          
40 0           codepoint = ((p[0] & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F);
41 1 50         } else if (len == 4 && (p[1] & 0xC0) == 0x80 && (p[2] & 0xC0) == 0x80 && (p[3] & 0xC0) == 0x80) {
    50          
    50          
    50          
42 1           codepoint = ((p[0] & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F);
43             } else {
44 0           p++;
45 0           continue;
46             }
47 9           sv_catpvf(out, "&#x%X;", (unsigned int)codepoint);
48 9           p += len;
49 9           continue;
50             }
51             // Do not escape HTML here, just copy ASCII
52 1326           sv_catpvf(out, "%c", *p);
53 1326           p++;
54             }
55 81           input = SvPV_nolen(out);
56 81           p = input; // prevent further processing, as we've already handled all input
57              
58             /* Preprocess: replace \r\n with \n in input */
59 81 50         if (opts->enable_preprocess) {
60 81           SV* pre = newSVpv("", 0);
61 81           const char* q = input;
62 1465 100         while (*q) {
63 1384 50         if (*q == '\r' && *(q+1) == '\n') {
    0          
64 0           sv_catpv(pre, "\n");
65 0           q += 2;
66             } else {
67 1384           sv_catpvf(pre, "%c", *q);
68 1384           q++;
69             }
70             }
71 81           input = SvPV_nolen(pre);
72 81           p = input;
73             }
74 81           out = newSVpv("", 0);
75              
76 464 100         while (*p) {
77             // Fenced code block
78 383 50         if (opts->enable_fenced_code && *p == '`' && *(p+1) == '`' && *(p+2) == '`') {
    100          
    100          
    50          
79 2           p += 3;
80             // Optional language specifier
81 2           const char* lang_start = p;
82 12 50         while (*p && *p != '\n' && *p != ' ') p++;
    100          
    50          
83 2           int lang_len = (int)(p-lang_start);
84 2 50         while (*p && *p != '\n') p++;
    50          
85             //if (*p == '\n') p++;
86 2           const char* code_start = p;
87 2           const char* fence = strstr(p, "```");
88 2 50         int code_len = fence ? (int)(fence - code_start) : (int)strlen(code_start);
89 2 100         if (lang_len > 0) {
90 1           sv_catpvf(out, "
%.*s
\n", lang_len, lang_start, code_len, code_start);
91             } else {
92 1           sv_catpvf(out, "
%.*s
\n", code_len, code_start);
93             }
94 2 50         if (fence) p = fence + 3;
95 2           continue;
96             }
97             // Strikethrough
98 381 50         if (opts->enable_strikethrough && *p == '~' && *(p+1) == '~') {
    100          
    50          
99 8           p += 2;
100 8           const char* start = p;
101 63 50         while (*p && !(*p == '~' && *(p+1) == '~')) p++;
    100          
    50          
102 8           sv_catpvf(out, "%.*s", (int)(p-start), start);
103 8 50         if (*p == '~' && *(p+1) == '~') p += 2;
    50          
104 8           continue;
105             }
106             // Headers
107 373 100         if (opts->enable_headers && *p == '#' && (*(p+1) == ' ' || *(p+1) == '#')) {
    100          
    100          
    100          
108 7           int level = 0;
109 20 100         while (*p == '#') { level++; p++; }
110 7 50         if (*p == ' ') p++;
111 7           const char* start = p;
112 105 100         while (*p && *p != '\n') p++;
    100          
113 7           sv_catpvf(out, "%.*s\n", level, (int)(p-start), start, level);
114 7 100         if (*p == '\n') p++;
115 7           continue;
116             }
117             // Task list
118 366 50         if (opts->enable_tasklist && *p == '-' && *(p+1) == ' ' && *(p+2) == '[' && (*(p+3) == ' ' || *(p+3) == 'x' || *(p+3) == 'X') && *(p+4) == ']') {
    100          
    50          
    100          
    100          
    50          
    0          
    50          
119 2 100         int checked = (*(p+3) == 'x' || *(p+3) == 'X');
    50          
120 2           p += 5;
121 2 50         if (*p == ' ') p++;
122 2           const char* start = p;
123 28 100         while (*p && *p != '\n') p++;
    50          
124 2 100         sv_catpvf(out, "
  • %.*s
  • \n", checked ? " checked" : "", (int)(p-start), start);
    125 2 50         if (*p == '\n') p++;
    126 2           continue;
    127             }
    128             // Table (very basic, only supports header row and one or more data rows)
    129 364 50         if (opts->enable_tables && *p == '|' && strchr(p, '\n')) {
        100          
        100          
    130             // Check if next line is a separator (|---|---|)
    131 3           const char* row_start = p;
    132 3           const char* nl = strchr(p, '\n');
    133 3 50         if (!nl) break;
    134 3           const char* sep = nl + 1;
    135 3 50         if (*sep == '|') {
    136 3           const char* sep_nl = strchr(sep, '\n');
    137 3 50         if (sep_nl && strstr(sep, "---") < sep_nl) {
        50          
    138             // Parse header
    139 3           sv_catpvf(out, "\n"); ", SvPV_nolen(cell_html)); \n"); "); ", SvPV_nolen(cell_html)); \n");
    140 3           const char* cell = row_start + 1;
    141 10 100         while (cell < nl) {
    142 7           const char* pipe = strchr(cell, '|');
    143 7 50         if (!pipe || pipe > nl) pipe = nl;
        50          
    144 9 100         while (*cell == ' ') cell++;
    145 7           const char* cell_end = pipe;
    146 9 50         while (cell_end > cell && (*(cell_end-1) == ' ')) cell_end--;
        100          
    147             // Recursively process header cell content for inline markdown (bold, italic, links, etc.)
    148 7           SV* cell_sv = newSVpv("", 0);
    149 7           sv_catpvf(cell_sv, "%.*s", (int)(cell_end-cell), cell);
    150 7           SV* cell_html = markdown_to_html(SvPV_nolen(cell_sv), opts);
    151 7           const char* html_str = SvPV_nolen(cell_html);
    152 7           STRLEN html_len = strlen(html_str);
    153             // Remove wrapping
    ...
    if present
    154 7 50         if (html_len > 11 && strncmp(html_str, "
    ", 5) == 0 && strncmp(html_str + html_len - 6, "
    ", 6) == 0) {
        50          
        50          
    155 7           SV* trimmed = newSVpv("", 0);
    156 7           sv_catpvn(trimmed, html_str + 5, html_len - 11);
    157 7           SvREFCNT_dec(cell_html);
    158 7           cell_html = trimmed;
    159             }
    160 7           sv_catpvf(out, "%s
    161 7           SvREFCNT_dec(cell_sv);
    162 7           SvREFCNT_dec(cell_html);
    163 7           cell = pipe + 1;
    164             }
    165 3           sv_catpvf(out, "
    166             // Parse rows
    167 3           const char* row = sep_nl + 1;
    168 5 100         while (*row == '|' && row) {
        50          
    169 3           const char* row_nl = strchr(row, '\n');
    170 3 100         if (!row_nl) row_nl = row + strlen(row);
    171 3           sv_catpvf(out, "
    172 3           const char* cell = row + 1;
    173 10 100         while (cell < row_nl) {
    174 7           const char* pipe = strchr(cell, '|');
    175 7 50         if (!pipe || pipe > row_nl) pipe = row_nl;
        50          
    176 9 100         while (*cell == ' ') cell++;
    177 7           const char* cell_end = pipe;
    178 13 50         while (cell_end > cell && (*(cell_end-1) == ' ')) cell_end--;
        100          
    179             // Recursively process table cell content for inline markdown (bold, italic, links, etc.)
    180 7           SV* cell_sv = newSVpv("", 0);
    181 7           sv_catpvf(cell_sv, "%.*s", (int)(cell_end-cell), cell);
    182 7           SV* cell_html = markdown_to_html(SvPV_nolen(cell_sv), opts);
    183 7           const char* html_str = SvPV_nolen(cell_html);
    184 7           STRLEN html_len = strlen(html_str);
    185             // Remove wrapping
    ...
    if present
    186 7 50         if (html_len > 11 && strncmp(html_str, "
    ", 5) == 0 && strncmp(html_str + html_len - 6, "
    ", 6) == 0) {
        50          
        50          
    187 7           SV* trimmed = newSVpv("", 0);
    188 7           sv_catpvn(trimmed, html_str + 5, html_len - 11);
    189 7           SvREFCNT_dec(cell_html);
    190 7           cell_html = trimmed;
    191             }
    192 7           sv_catpvf(out, "%s
    193 7           SvREFCNT_dec(cell_sv);
    194 7           SvREFCNT_dec(cell_html);
    195 7           cell = pipe + 1;
    196             }
    197 3           sv_catpvf(out, "
    198 3 100         if (*row_nl == '\0') break;
    199 2           row = row_nl + 1;
    200             }
    201 3           sv_catpvf(out, "
    \n");
    202 3           p = row;
    203 3           continue;
    204             }
    205             }
    206             }
    207             // Bold
    208 361 100         if (opts->enable_bold && (
    209 353 100         (*p == '*' && *(p+1) == '*') ||
        100          
    210 343 100         (*p == '_' && *(p+1) == '_')
        100          
    211 19           )) {
    212 19           p += 2;
    213 19           const char* start = p;
    214 98 50         while (*p && !(
    215 98 100         ((*p == '*' && *(p+1) == '*')) ||
        50          
    216 88 100         ((*p == '_' && *(p+1) == '_'))
        50          
    217 79           )) p++;
    218 19           sv_catpvf(out, "%.*s", (int)(p-start), start);
    219 19 100         if ((*p == '*' && *(p+1) == '*') || (*p == '_' && *(p+1) == '_')) p += 2;
        50          
        50          
        50          
    220 19           continue;
    221             }
    222             // Italic
    223 342 100         if (opts->enable_italic && ((*p == '*' && *(p+1) != ' ') || (*p == '_' && *(p+1) != ' '))) {
        100          
        100          
        100          
        50          
    224 19           p++;
    225 19           const char* start = p;
    226 19           char end_marker = *(p-1); // either '*' or '_'
    227 134 50         while (*p && *p != end_marker) p++;
        100          
    228 19           sv_catpvf(out, "%.*s", (int)(p-start), start);
    229 19 50         if (*p == end_marker) p++;
    230 19           continue;
    231             }
    232             // Inline code
    233 323 50         if (opts->enable_code && *p == '`') {
        100          
    234 2           p++;
    235 2           const char* start = p;
    236 10 50         while (*p && *p != '`') p++;
        100          
    237 2           sv_catpvf(out, "%.*s", (int)(p-start), start);
    238 2 50         if (*p == '`') p++;
    239 2           continue;
    240             }
    241             // Images
    242 321 50         if (opts->enable_images && *p == '!' && *(p+1) == '[') {
        100          
        50          
    243 1           p += 2;
    244 1           const char* alt_start = p;
    245 9 50         while (*p && *p != ']') p++;
        100          
    246 1           int alt_len = (int)(p-alt_start);
    247 1 50         if (*p == ']') p++;
    248 1 50         if (*p == '(') {
    249 1           p++;
    250 1           const char* url_start = p;
    251 10 50         while (*p && *p != ')') p++;
        100          
    252 1           int url_len = (int)(p-url_start);
    253 1           sv_catpvf(out, "\"%.*s\"", alt_len, alt_start, url_len, url_start);
    254 1 50         if (*p == ')') p++;
    255 1           continue;
    256             }
    257             }
    258             // Links
    259 320 50         if (opts->enable_links && *p == '[') {
        100          
    260 1           p++;
    261 1           const char* text_start = p;
    262 10 50         while (*p && *p != ']') p++;
        100          
    263 1           int text_len = (int)(p-text_start);
    264 1 50         if (*p == ']') p++;
    265 1 50         if (*p == '(') {
    266 1           p++;
    267 1           const char* url_start = p;
    268 19 50         while (*p && *p != ')') p++;
        100          
    269 1           int url_len = (int)(p-url_start);
    270 1           sv_catpvf(out, "%.*s", url_len, url_start, text_len, text_start);
    271 1 50         if (*p == ')') p++;
    272 1           continue;
    273             }
    274             }
    275             // Ordered lists
    276             // Ordered lists (support multiple consecutive lines as one
      )
    277 319 50         if (opts->enable_ordered_lists &&
        100          
    278 265 100         (p == input || *(p-1) == '\n') &&
    279 71 100         *p >= '1' && *p <= '9' &&
        100          
    280 3 50         (*(p+1) == '.' || *(p+1) == ')')) {
        0          
    281 3           sv_catpvf(out, "
      \n");
    282 7 50         while (opts->enable_ordered_lists && *p >= '1' && *p <= '9' && (*(p+1) == '.' || *(p+1) == ')')) {
        50          
        50          
        50          
        0          
    283 7           p += 2; // Skip number and dot/parenthesis
    284 7 50         if (*p == ' ') p++; // Skip space
    285 7           const char* start = p;
    286 68 100         while (*p && *p != '\n') p++;
        100          
    287             // Recursively process the list item content for inline markdown (bold, italic, links, etc.)
    288 7           SV* item_sv = newSVpv("", 0);
    289 7           sv_catpvf(item_sv, "%.*s", (int)(p-start), start);
    290 7           SV* item_html = markdown_to_html(SvPV_nolen(item_sv), opts);
    291             /* Remove wrapping
    ...
    if present */
    292 7           const char* html_str = SvPV_nolen(item_html);
    293 7           STRLEN html_len = strlen(html_str);
    294 7 50         if (html_len > 11 && strncmp(html_str, "
    ", 5) == 0 && strncmp(html_str + html_len - 6, "
    ", 6) == 0) {
        50          
        50          
    295 7           SV* trimmed = newSVpv("", 0);
    296 7           sv_catpvn(trimmed, html_str + 5, html_len - 11);
    297 7           SvREFCNT_dec(item_html);
    298 7           item_html = trimmed;
    299             }
    300 7           sv_catpvf(out, "
  • %s
  • \n", SvPV_nolen(item_html));
    301 7           SvREFCNT_dec(item_sv);
    302 7           SvREFCNT_dec(item_html);
    303 7 100         if (*p == '\n') p++;
    304             // Skip blank lines between list items
    305 7           const char* lookahead = p;
    306 7 50         while (*lookahead == '\n') lookahead++;
    307 7 100         if (!(*lookahead >= '1' && *lookahead <= '9' && (*(lookahead+1) == '.' || *(lookahead+1) == ')')))
        50          
        50          
        0          
    308             break;
    309 4           p = lookahead;
    310             }
    311 3           sv_catpvf(out, "\n");
    312 3           continue;
    313             }
    314             // Unordered lists
    315             // Unordered lists (support multiple consecutive lines as one
      )
    316 316 100         if (opts->enable_unordered_lists &&
        100          
    317 249 100         (p == input || *(p-1) == '\n') &&
    318 66 100         (*p == '-' || *p == '*' || *p == '+') && (*(p+1) == ' ')) {
        100          
        50          
        50          
    319 7           sv_catpvf(out, "
      \n");
    320 16 50         while (opts->enable_unordered_lists && (*p == '-' || *p == '*' || *p == '+') && (*(p+1) == ' ')) {
        100          
        50          
        0          
        100          
    321 14           p++; // Skip marker
    322 14 50         if (*p == ' ') p++; // Skip space
    323 14           const char* start = p;
    324 131 100         while (*p && *p != '\n') p++;
        100          
    325             // Recursively process the list item content for inline markdown (bold, italic, links, etc.)
    326 14           SV* item_sv = newSVpv("", 0);
    327 14           sv_catpvf(item_sv, "%.*s", (int)(p-start), start);
    328 14           SV* item_html = markdown_to_html(SvPV_nolen(item_sv), opts);
    329 14           SvREFCNT_dec(item_html);
    330 14           item_html = markdown_to_html(SvPV_nolen(item_sv), opts);
    331             /* Remove wrapping
    ...
    if present */
    332 14           const char* html_str = SvPV_nolen(item_html);
    333 14           STRLEN html_len = strlen(html_str);
    334 14 50         if (html_len > 11 && strncmp(html_str, "
    ", 5) == 0 && strncmp(html_str + html_len - 6, "
    ", 6) == 0) {
        50          
        50          
    335 14           SV* trimmed = newSVpv("", 0);
    336 14           sv_catpvn(trimmed, html_str + 5, html_len - 11);
    337 14           SvREFCNT_dec(item_html);
    338 14           item_html = trimmed;
    339             }
    340 14           sv_catpvf(out, "
  • %s
  • \n", SvPV_nolen(item_html));
    341 14           SvREFCNT_dec(item_sv);
    342 14           SvREFCNT_dec(item_html);
    343 14 100         if (*p == '\n') p++;
    344             // Skip blank lines between list items
    345 14           const char* lookahead = p;
    346 14 50         while (*lookahead == '\n') lookahead++;
    347 14 100         if (!(*lookahead == '-' || *lookahead == '*' || *lookahead == '+'))
        100          
        50          
    348 5           break;
    349 9           p = lookahead;
    350             }
    351 7           sv_catpvf(out, "\n");
    352 7           continue;
    353             }
    354             // Default: copy character
    355 309           sv_catpvf(out, "%c", *p);
    356 309           p++;
    357             }
    358              
    359             /* Split output on double newlines and wrap each part in
    ...
    */
    360             STRLEN len;
    361 81           char* html = SvPV(out, len);
    362              
    363             /* Unescape all UTF-8 characters (convert &#x...; back to UTF-8), but do not touch HTML entities */
    364 81           SV* unescaped = newSVpv("", 0);
    365 81           char* scan = html;
    366 2656 100         while (*scan) {
    367 2575 100         if (scan[0] == '&' && scan[1] == '#' && scan[2] == 'x') {
        50          
        50          
    368 9           char* semi = strchr(scan, ';');
    369 9 50         if (semi && semi - scan < 10) {
        50          
    370 9           unsigned int codepoint = 0;
    371 9 50         if (sscanf(scan + 3, "%X", &codepoint) == 1) {
    372             /* Write codepoint as UTF-8 */
    373             unsigned char utf8buf[4];
    374 9           int utf8len = 0;
    375 9 50         if (codepoint <= 0x7F) {
    376 0           utf8buf[0] = (unsigned char)codepoint;
    377 0           utf8len = 1;
    378 9 100         } else if (codepoint <= 0x7FF) {
    379 8           utf8buf[0] = 0xC0 | ((codepoint >> 6) & 0x1F);
    380 8           utf8buf[1] = 0x80 | (codepoint & 0x3F);
    381 8           utf8len = 2;
    382 1 50         } else if (codepoint <= 0xFFFF) {
    383 0           utf8buf[0] = 0xE0 | ((codepoint >> 12) & 0x0F);
    384 0           utf8buf[1] = 0x80 | ((codepoint >> 6) & 0x3F);
    385 0           utf8buf[2] = 0x80 | (codepoint & 0x3F);
    386 0           utf8len = 3;
    387 1 50         } else if (codepoint <= 0x10FFFF) {
    388 1           utf8buf[0] = 0xF0 | ((codepoint >> 18) & 0x07);
    389 1           utf8buf[1] = 0x80 | ((codepoint >> 12) & 0x3F);
    390 1           utf8buf[2] = 0x80 | ((codepoint >> 6) & 0x3F);
    391 1           utf8buf[3] = 0x80 | (codepoint & 0x3F);
    392 1           utf8len = 4;
    393             }
    394 9           sv_catpvn(unescaped, (char*)utf8buf, utf8len);
    395 9           scan = semi + 1;
    396 9           continue;
    397             }
    398             }
    399             }
    400 2566           sv_catpvn(unescaped, scan, 1);
    401 2566           scan++;
    402             }
    403 81           html = SvPV_nolen(unescaped);
    404              
    405 81           SV* final = newSVpv("", 0);
    406 81           char* start = html;
    407             char* end;
    408 89 100         while ((end = strstr(start, "\n\n"))) {
    409 8           int part_len = (int)(end - start);
    410 8 50         if (part_len > 0) {
    411 8           sv_catpvf(final, "
    %.*s
    ", part_len, start);
    412             }
    413 8           start = end + 2;
    414             }
    415 81 100         if (*start) {
    416 80           sv_catpvf(final, "
    %s
    ", start);
    417             }
    418             /* Remove all newlines from the final output */
    419             STRLEN final_len;
    420 81           char* final_html = SvPV(final, final_len);
    421 81           SV* no_newlines = newSVpv("", 0);
    422 81           int in_code = 0;
    423 3608 100         for (STRLEN i = 0; i < final_len; ) {
    424             // Detect start of code block
    425 3527 100         if (!in_code && i + 5 < final_len && strncmp(final_html + i, "
    
    
        100          
        100          
    426 2           in_code = 1;
    427             }
    428             // Detect end of code block
    429 3527 100         if (in_code && i + 13 < final_len && strncmp(final_html + i, "", 13) == 0) {
        50          
        100          
    430 2           in_code = 0;
    431             }
    432 3527           STRLEN char_len = 1;
    433 3527           unsigned char c = (unsigned char)final_html[i];
    434 3527 100         if (c >= 0x80) {
    435 9 100         if ((c & 0xE0) == 0xC0) char_len = 2;
    436 1 50         else if ((c & 0xF0) == 0xE0) char_len = 3;
    437 1 50         else if ((c & 0xF8) == 0xF0) char_len = 4;
    438             }
    439 3527 100         if (in_code || (final_html[i] != '\n' && final_html[i] != '\r')) {
        100          
        50          
    440 3452           sv_catpvn(no_newlines, final_html + i, char_len);
    441             }
    442 3527           i += char_len;
    443             }
    444 81           SvREFCNT_dec(final);
    445 81           final = no_newlines;
    446 81           SvREFCNT_dec(out);
    447 81           out = final;
    448 81           return out;
    449             }
    450              
    451              
    452 2           static SV* strip_markdown_except_lists_tables(const char* input) {
    453 2           SV* out = newSVpv("", 0);
    454 2           const char* p = input;
    455              
    456 134 100         while (*p) {
    457             // Ordered lists: keep numbers and text, remove ". " or ") "
    458 132 100         if ((p == input || *(p-1) == '\n') && *p >= '1' && *p <= '9' && (*(p+1) == '.' || *(p+1) == ')')) {
        100          
        100          
        50          
        0          
        0          
    459             // Copy number
    460 0           sv_catpvn(out, p, 1);
    461 0           sv_catpvn(out, " ", 1); // Keep space after number
    462 0           p += 2;
    463 0 0         if (*p == ' ') p++;
    464 0           continue;
    465             }
    466             // Unordered lists: keep marker, remove space
    467 132 100         if ((p == input || *(p-1) == '\n') && (*p == '-' || *p == '*' || *p == '+') && *(p+1) == ' ') {
        100          
        50          
        100          
        50          
        100          
    468 2           sv_catpvn(out, p, 1);
    469 2           sv_catpvn(out, " ", 1); // Keep space after marker
    470 2           p += 2;
    471 2           continue;
    472             }
    473             // Tables: just copy everything (including pipes and dashes)
    474 130 100         if (*p == '|') {
    475 12           sv_catpvn(out, p, 1);
    476 12           p++;
    477 12           continue;
    478             }
    479             // Table separator row (---): just copy dashes and pipes
    480 118 100         if (*p == '-' && ((p > input && *(p-1) == '|') || (p == input))) {
        50          
        100          
        50          
    481 3           sv_catpvn(out, p, 1);
    482 3           p++;
    483 3           continue;
    484             }
    485             // Remove bold (** or __)
    486 115 100         if ((*p == '*' && *(p+1) == '*') || (*p == '_' && *(p+1) == '_')) {
        50          
        100          
        100          
    487 10           p += 2;
    488 10           continue;
    489             }
    490             // Remove italic (* or _)
    491 105 50         if (*p == '*' || *p == '_') {
        100          
    492 4           p++;
    493 4           continue;
    494             }
    495             // Remove strikethrough (~~)
    496 101 50         if (*p == '~' && *(p+1) == '~') {
        0          
    497 0           p += 2;
    498 0           continue;
    499             }
    500             // Remove inline code (`) and fenced code (```)
    501 101 50         if (*p == '`') {
    502 0 0         if (*(p+1) == '`' && *(p+2) == '`') {
        0          
    503 0           p += 3;
    504             // Skip until closing ```
    505 0           const char* fence = strstr(p, "```");
    506 0 0         if (fence) {
    507 0           p = fence + 3;
    508             } else {
    509 0           p += strlen(p);
    510             }
    511             } else {
    512 0           p++;
    513             // Skip until closing `
    514 0 0         while (*p && *p != '`') p++;
        0          
    515 0 0         if (*p == '`') p++;
    516             }
    517 0           continue;
    518             }
    519             // Remove images ![alt](url)
    520 101 50         if (*p == '!' && *(p+1) == '[') {
        0          
    521 0           p += 2;
    522 0 0         while (*p && *p != ']') p++;
        0          
    523 0 0         if (*p == ']') p++;
    524 0 0         if (*p == '(') {
    525 0           p++;
    526 0 0         while (*p && *p != ')') p++;
        0          
    527 0 0         if (*p == ')') p++;
    528             }
    529 0           continue;
    530             }
    531             // Remove links [text](url), keep text
    532 101 50         if (*p == '[') {
    533 0           p++;
    534 0           const char* text_start = p;
    535 0 0         while (*p && *p != ']') p++;
        0          
    536 0           int text_len = (int)(p - text_start);
    537 0 0         if (text_len > 0)
    538 0           sv_catpvn(out, text_start, text_len);
    539 0 0         if (*p == ']') p++;
    540 0 0         if (*p == '(') {
    541 0           p++;
    542 0 0         while (*p && *p != ')') p++;
        0          
    543 0 0         if (*p == ')') p++;
    544             }
    545 0           continue;
    546             }
    547             // Remove headers (#)
    548 101 100         if (*p == '#') {
    549 6 100         while (*p == '#') p++;
    550 3 50         if (*p == ' ') p++;
    551 3           continue;
    552             }
    553             // Remove task list [ ] or [x]
    554 98 50         if (*p == '[' && (*(p+1) == ' ' || *(p+1) == 'x' || *(p+1) == 'X') && *(p+2) == ']') {
        0          
        0          
        0          
        0          
    555 0           p += 3;
    556 0 0         if (*p == ' ') p++;
    557 0           continue;
    558             }
    559             // Default: copy character
    560 98           sv_catpvn(out, p, 1);
    561 98           p++;
    562             }
    563 2           return out;
    564             }
    565              
    566             MODULE = Markdown::Simple PACKAGE = Markdown::Simple
    567              
    568             SV*
    569             strip_markdown(input)
    570             const char* input
    571             CODE:
    572 2           RETVAL = strip_markdown_except_lists_tables(input);
    573             OUTPUT:
    574             RETVAL
    575              
    576             SV*
    577             markdown_to_html(input, ...)
    578             const char* input
    579             PREINIT:
    580 32           MarkdownOptions opts = {1, 1,1,1,1,1,1,1,1,1,1,1,1};
    581             HV* options;
    582             SV** val;
    583             CODE:
    584 32 100         if (items > 1 && SvOK(ST(1)) && SvROK(ST(1)) && SvTYPE(SvRV(ST(1))) == SVt_PVHV) {
        50          
        50          
        50          
    585 3           options = (HV*)SvRV(ST(1));
    586 3 100         if ((val = hv_fetch(options, "headers", 7, 0)) && SvOK(*val)) opts.enable_headers = SvTRUE(*val);
        50          
    587 3 100         if ((val = hv_fetch(options, "bold", 4, 0)) && SvOK(*val)) opts.enable_bold = SvTRUE(*val);
        50          
    588 3 100         if (val = hv_fetch(options, "italic", 6, 0)) opts.enable_italic = SvTRUE(*val);
    589 3 50         if (val = hv_fetch(options, "links", 5, 0)) opts.enable_links = SvTRUE(*val);
    590 3 50         if (val = hv_fetch(options, "images", 6, 0)) opts.enable_images = SvTRUE(*val);
    591 3 50         if (val = hv_fetch(options, "code", 4, 0)) opts.enable_code = SvTRUE(*val);
    592 3 50         if (val = hv_fetch(options, "tables", 6, 0)) opts.enable_tables = SvTRUE(*val);
    593 3 50         if (val = hv_fetch(options, "tasklist", 8, 0)) opts.enable_tasklist = SvTRUE(*val);
    594 3 50         if (val = hv_fetch(options, "fenced_code", 11, 0)) opts.enable_fenced_code = SvTRUE(*val);
    595 3 50         if (val = hv_fetch(options, "strikethrough", 13, 0)) opts.enable_strikethrough = SvTRUE(*val);
    596 3 50         if (val = hv_fetch(options, "ordered_lists", 13, 0)) opts.enable_ordered_lists = SvTRUE(*val);
    597 3 100         if (val = hv_fetch(options, "unordered_lists", 15, 0)) opts.enable_unordered_lists = SvTRUE(*val);
    598 3 50         if (val = hv_fetch(options, "preprocess", 10, 0)) opts.enable_preprocess = SvTRUE(*val);
    599             }
    600 32           RETVAL = markdown_to_html(input, &opts);
    601             OUTPUT:
    602             RETVAL