apex-ruby 1.0.38 → 1.0.40
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee084bd77c91c213aa431c0485b23ec3a15106ee3f341d92e5885f29c8351c93
|
|
4
|
+
data.tar.gz: 7840707f775f33a7562734f60e5d69db520a93d98e95d9450483e684972d01e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3e0f7af016badd58f00b1fa33742c4ee87ec6a369480e62867252fb3a13a5d3fb6b994abb63931860ddfbf94bed8dc50f959ad2c15d900d8c05c0499b99fd05
|
|
7
|
+
data.tar.gz: dbb6ea6f6087e1bc558d1fe55003061bb7ad76b6992a6ff80197ef6edf1abb3839c5ee494afaebcb7c421740badd4a675d0dbda8585ed73f524b4186f5c9b4f6
|
|
@@ -21,8 +21,8 @@ typedef struct cmark_node cmark_node;
|
|
|
21
21
|
|
|
22
22
|
#define APEX_VERSION_MAJOR 1
|
|
23
23
|
#define APEX_VERSION_MINOR 1
|
|
24
|
-
#define APEX_VERSION_PATCH
|
|
25
|
-
#define APEX_VERSION_STRING "1.1.
|
|
24
|
+
#define APEX_VERSION_PATCH 43
|
|
25
|
+
#define APEX_VERSION_STRING "1.1.43"
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Processor compatibility modes
|
|
@@ -278,23 +278,490 @@ void apex_process_critic_markup_in_tree(cmark_node *node, critic_mode_t mode) {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
static const char *critic_next_line(const char *p) {
|
|
282
|
+
while (*p && *p != '\n' && *p != '\r') p++;
|
|
283
|
+
if (*p == '\r' && p[1] == '\n') return p + 2;
|
|
284
|
+
return *p ? p + 1 : p;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Length of a ``` or ~~~ fence run opening the line, or 0. */
|
|
288
|
+
static size_t critic_fence_run(const char *line, char *fence_char, const char **after_run) {
|
|
289
|
+
const char *p = line;
|
|
290
|
+
while (*p == ' ' || *p == '\t' || *p == '>') p++;
|
|
291
|
+
if (*p != '`' && *p != '~') return 0;
|
|
292
|
+
char c = *p;
|
|
293
|
+
const char *run = p;
|
|
294
|
+
while (*p == c) p++;
|
|
295
|
+
if (p - run < 3) return 0;
|
|
296
|
+
*fence_char = c;
|
|
297
|
+
*after_run = p;
|
|
298
|
+
return (size_t)(p - run);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* Bytes at pos that are a fenced block or code span and must be copied
|
|
302
|
+
* through untouched. Returns 0 when pos does not start code. */
|
|
303
|
+
static size_t critic_code_region_len(const char *base, const char *pos) {
|
|
304
|
+
bool line_start = (pos == base || pos[-1] == '\n' || pos[-1] == '\r');
|
|
305
|
+
if (line_start) {
|
|
306
|
+
char fence_char;
|
|
307
|
+
const char *after;
|
|
308
|
+
size_t open_len = critic_fence_run(pos, &fence_char, &after);
|
|
309
|
+
if (open_len) {
|
|
310
|
+
bool valid = true;
|
|
311
|
+
if (fence_char == '`') {
|
|
312
|
+
for (const char *q = after; *q && *q != '\n' && *q != '\r'; q++) {
|
|
313
|
+
if (*q == '`') { valid = false; break; }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (valid) {
|
|
317
|
+
const char *line = critic_next_line(pos);
|
|
318
|
+
while (*line) {
|
|
319
|
+
char close_char;
|
|
320
|
+
const char *close_after;
|
|
321
|
+
size_t close_len = critic_fence_run(line, &close_char, &close_after);
|
|
322
|
+
if (close_len >= open_len && close_char == fence_char) {
|
|
323
|
+
while (*close_after == ' ' || *close_after == '\t') close_after++;
|
|
324
|
+
if (!*close_after || *close_after == '\n' || *close_after == '\r') {
|
|
325
|
+
return (size_t)(critic_next_line(line) - pos);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
line = critic_next_line(line);
|
|
329
|
+
}
|
|
330
|
+
/* Unclosed fence: protect only the opening line */
|
|
331
|
+
return (size_t)(critic_next_line(pos) - pos);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (*pos != '`') return 0;
|
|
337
|
+
|
|
338
|
+
size_t backslashes = 0;
|
|
339
|
+
for (const char *b = pos; b > base && b[-1] == '\\'; b--) backslashes++;
|
|
340
|
+
if (backslashes % 2) return 1;
|
|
341
|
+
|
|
342
|
+
const char *p = pos;
|
|
343
|
+
while (*p == '`') p++;
|
|
344
|
+
size_t open_len = (size_t)(p - pos);
|
|
345
|
+
|
|
346
|
+
/* Code spans may wrap lines but end at a blank line */
|
|
347
|
+
while (*p) {
|
|
348
|
+
if (*p == '\n' || *p == '\r') {
|
|
349
|
+
const char *next = critic_next_line(p);
|
|
350
|
+
const char *q = next;
|
|
351
|
+
while (*q == ' ' || *q == '\t' || *q == '>') q++;
|
|
352
|
+
if (!*q || *q == '\n' || *q == '\r') break;
|
|
353
|
+
p = next;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (*p == '`') {
|
|
357
|
+
const char *run = p;
|
|
358
|
+
while (*p == '`') p++;
|
|
359
|
+
if ((size_t)(p - run) == open_len) return (size_t)(p - pos);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
p++;
|
|
363
|
+
}
|
|
364
|
+
return open_len;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Indented code blocks follow the CommonMark rules: four spaces or a tab
|
|
368
|
+
* at the current container, and they cannot interrupt a paragraph. List
|
|
369
|
+
* continuations and paragraph lines stay eligible for critic markup. */
|
|
370
|
+
|
|
371
|
+
typedef enum {
|
|
372
|
+
CRITIC_BOX_QUOTE,
|
|
373
|
+
CRITIC_BOX_ITEM
|
|
374
|
+
} critic_box_kind;
|
|
375
|
+
|
|
376
|
+
typedef struct {
|
|
377
|
+
critic_box_kind kind;
|
|
378
|
+
int need; /* ITEM: columns required to continue */
|
|
379
|
+
bool has_content; /* ITEM has a child block */
|
|
380
|
+
} critic_box;
|
|
381
|
+
|
|
382
|
+
typedef struct {
|
|
383
|
+
critic_box stack[32];
|
|
384
|
+
int depth;
|
|
385
|
+
bool in_paragraph;
|
|
386
|
+
bool in_code;
|
|
387
|
+
} critic_blocks;
|
|
388
|
+
|
|
389
|
+
static int critic_col_add(int col, char c) {
|
|
390
|
+
if (c == '\t') return col + (4 - (col % 4));
|
|
391
|
+
return col + 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
static bool critic_line_blank(const char *p) {
|
|
395
|
+
while (*p == ' ' || *p == '\t') p++;
|
|
396
|
+
return *p == '\0' || *p == '\n' || *p == '\r';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
static const char *critic_skip_indent(const char *p, int *col, int *indent) {
|
|
400
|
+
int start = *col;
|
|
401
|
+
while (*p == ' ' || *p == '\t') {
|
|
402
|
+
*col = critic_col_add(*col, *p);
|
|
403
|
+
p++;
|
|
404
|
+
}
|
|
405
|
+
*indent = *col - start;
|
|
406
|
+
return p;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* Consume exactly `columns` of indent. A tab may be only partly consumed. */
|
|
410
|
+
static const char *critic_advance_columns(const char *p, int *col, int columns) {
|
|
411
|
+
int target = *col + columns;
|
|
412
|
+
while (*p && *p != '\n' && *p != '\r' && *col < target) {
|
|
413
|
+
if (*p == '\t') {
|
|
414
|
+
int next = critic_col_add(*col, '\t');
|
|
415
|
+
if (next > target) {
|
|
416
|
+
*col = target;
|
|
417
|
+
return p;
|
|
418
|
+
}
|
|
419
|
+
*col = next;
|
|
420
|
+
p++;
|
|
421
|
+
} else {
|
|
422
|
+
*col += 1;
|
|
423
|
+
p++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return p;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static bool critic_match_quote(const char *p, int col, const char **out, int *out_col) {
|
|
430
|
+
int c = col;
|
|
431
|
+
int indent = 0;
|
|
432
|
+
const char *q = critic_skip_indent(p, &c, &indent);
|
|
433
|
+
if (indent > 3 || *q != '>') return false;
|
|
434
|
+
q++;
|
|
435
|
+
c += 1;
|
|
436
|
+
if (*q == ' ' || *q == '\t') {
|
|
437
|
+
c = critic_col_add(c, *q);
|
|
438
|
+
q++;
|
|
439
|
+
}
|
|
440
|
+
*out = q;
|
|
441
|
+
*out_col = c;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* 1 = bullet, 2 = ordered. len is the marker without trailing space. */
|
|
446
|
+
static int critic_list_marker(const char *p, int *len, int *ordered_start) {
|
|
447
|
+
if (*p == '-' || *p == '+' || *p == '*') {
|
|
448
|
+
char n = p[1];
|
|
449
|
+
if (n == ' ' || n == '\t' || n == '\n' || n == '\r' || n == '\0') {
|
|
450
|
+
*len = 1;
|
|
451
|
+
*ordered_start = 0;
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
if (*p >= '1' && *p <= '9') {
|
|
457
|
+
int i = 0;
|
|
458
|
+
int num = 0;
|
|
459
|
+
while (i < 9 && p[i] >= '0' && p[i] <= '9') {
|
|
460
|
+
num = num * 10 + (p[i] - '0');
|
|
461
|
+
i++;
|
|
462
|
+
}
|
|
463
|
+
if (i > 0 && (p[i] == '.' || p[i] == ')')) {
|
|
464
|
+
char n = p[i + 1];
|
|
465
|
+
if (n == ' ' || n == '\t' || n == '\n' || n == '\r' || n == '\0') {
|
|
466
|
+
*len = i + 1;
|
|
467
|
+
*ordered_start = num;
|
|
468
|
+
return 2;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static bool critic_is_atx(const char *p) {
|
|
476
|
+
int n = 0;
|
|
477
|
+
while (p[n] == '#' && n < 7) n++;
|
|
478
|
+
if (n < 1 || n > 6) return false;
|
|
479
|
+
return p[n] == ' ' || p[n] == '\t' || p[n] == '\n' || p[n] == '\r' || p[n] == '\0';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
static bool critic_is_fence_opener(const char *p) {
|
|
483
|
+
char c = *p;
|
|
484
|
+
if (c != '`' && c != '~') return false;
|
|
485
|
+
int n = 0;
|
|
486
|
+
while (p[n] == c) n++;
|
|
487
|
+
if (n < 3) return false;
|
|
488
|
+
if (c == '`') {
|
|
489
|
+
for (int i = n; p[i] && p[i] != '\n' && p[i] != '\r'; i++) {
|
|
490
|
+
if (p[i] == '`') return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* Thematic break, or a setext underline when a paragraph is open. */
|
|
497
|
+
static bool critic_is_rule(const char *p, bool in_paragraph) {
|
|
498
|
+
char kind = 0;
|
|
499
|
+
int count = 0;
|
|
500
|
+
bool spaced = false;
|
|
501
|
+
for (const char *q = p; *q && *q != '\n' && *q != '\r'; q++) {
|
|
502
|
+
if (*q == ' ' || *q == '\t') {
|
|
503
|
+
if (count > 0) spaced = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (*q != '-' && *q != '_' && *q != '*' && *q != '=') return false;
|
|
507
|
+
if (!kind) kind = *q;
|
|
508
|
+
else if (*q != kind) return false;
|
|
509
|
+
count++;
|
|
510
|
+
}
|
|
511
|
+
if (count < 1 || !kind) return false;
|
|
512
|
+
if (!spaced && in_paragraph && (kind == '-' || kind == '=') && count >= 1) return true;
|
|
513
|
+
if (!spaced && count >= 3 && (kind == '-' || kind == '_' || kind == '*')) return true;
|
|
514
|
+
if (spaced && count >= 3 && (kind == '-' || kind == '_' || kind == '*')) return true;
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
static int critic_match_boxes(const char *line, const critic_blocks *st,
|
|
519
|
+
const char **p, int *col) {
|
|
520
|
+
*p = line;
|
|
521
|
+
*col = 0;
|
|
522
|
+
int matched = 0;
|
|
523
|
+
bool blank = critic_line_blank(line);
|
|
524
|
+
for (int i = 0; i < st->depth; i++) {
|
|
525
|
+
if (st->stack[i].kind == CRITIC_BOX_QUOTE) {
|
|
526
|
+
const char *q;
|
|
527
|
+
int c;
|
|
528
|
+
if (!critic_match_quote(*p, *col, &q, &c)) break;
|
|
529
|
+
*p = q;
|
|
530
|
+
*col = c;
|
|
531
|
+
matched++;
|
|
532
|
+
} else if (blank) {
|
|
533
|
+
if (!st->stack[i].has_content) break;
|
|
534
|
+
matched++;
|
|
535
|
+
} else {
|
|
536
|
+
int indent = 0;
|
|
537
|
+
int c = *col;
|
|
538
|
+
critic_skip_indent(*p, &c, &indent);
|
|
539
|
+
if (indent < st->stack[i].need) break;
|
|
540
|
+
c = *col;
|
|
541
|
+
*p = critic_advance_columns(*p, &c, st->stack[i].need);
|
|
542
|
+
*col = c;
|
|
543
|
+
matched++;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return matched;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
static void critic_mark_content(critic_blocks *st) {
|
|
550
|
+
for (int i = 0; i < st->depth; i++) {
|
|
551
|
+
if (st->stack[i].kind == CRITIC_BOX_ITEM) st->stack[i].has_content = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* Would this line open a block that closes an open paragraph? */
|
|
556
|
+
static bool critic_interrupts_paragraph(const char *p, int col) {
|
|
557
|
+
int indent = 0;
|
|
558
|
+
int c = col;
|
|
559
|
+
const char *q = critic_skip_indent(p, &c, &indent);
|
|
560
|
+
if (indent >= 4) return false; /* indented code cannot interrupt */
|
|
561
|
+
if (*q == '>') return true;
|
|
562
|
+
int marker_len = 0;
|
|
563
|
+
int ordered_start = 0;
|
|
564
|
+
int kind = critic_list_marker(q, &marker_len, &ordered_start);
|
|
565
|
+
if (kind == 1) return true;
|
|
566
|
+
if (kind == 2 && ordered_start == 1) return true;
|
|
567
|
+
if (critic_is_atx(q) || critic_is_fence_opener(q) || critic_is_rule(q, true)) return true;
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Interpret the remainder of a line after open containers have been matched.
|
|
572
|
+
* Returns true when the line is indented code and must be copied verbatim. */
|
|
573
|
+
static bool critic_interpret_line(critic_blocks *st, const char *p, int col) {
|
|
574
|
+
for (;;) {
|
|
575
|
+
int indent = 0;
|
|
576
|
+
int c = col;
|
|
577
|
+
const char *q = critic_skip_indent(p, &c, &indent);
|
|
578
|
+
if (*q == '\0' || *q == '\n' || *q == '\r') {
|
|
579
|
+
st->in_paragraph = false;
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
if (indent >= 4) {
|
|
583
|
+
if (!st->in_paragraph) {
|
|
584
|
+
st->in_code = true;
|
|
585
|
+
critic_mark_content(st);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (*q == '>') {
|
|
591
|
+
if (st->depth < 32) {
|
|
592
|
+
st->stack[st->depth].kind = CRITIC_BOX_QUOTE;
|
|
593
|
+
st->stack[st->depth].need = 0;
|
|
594
|
+
st->stack[st->depth].has_content = false;
|
|
595
|
+
st->depth++;
|
|
596
|
+
}
|
|
597
|
+
q++;
|
|
598
|
+
c += 1;
|
|
599
|
+
if (*q == ' ' || *q == '\t') {
|
|
600
|
+
c = critic_col_add(c, *q);
|
|
601
|
+
q++;
|
|
602
|
+
}
|
|
603
|
+
p = q;
|
|
604
|
+
col = c;
|
|
605
|
+
st->in_paragraph = false;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
int marker_len = 0;
|
|
609
|
+
int ordered_start = 0;
|
|
610
|
+
int kind = critic_list_marker(q, &marker_len, &ordered_start);
|
|
611
|
+
if (kind && !(st->in_paragraph && kind == 2 && ordered_start != 1)) {
|
|
612
|
+
int marker_offset = indent;
|
|
613
|
+
const char *after = q + marker_len;
|
|
614
|
+
int after_col = c + marker_len;
|
|
615
|
+
int save_col = after_col;
|
|
616
|
+
const char *s = after;
|
|
617
|
+
int sc = after_col;
|
|
618
|
+
while (*s == ' ' || *s == '\t') {
|
|
619
|
+
if (sc - save_col > 5) break;
|
|
620
|
+
int next = critic_col_add(sc, *s);
|
|
621
|
+
if (sc - save_col <= 5) {
|
|
622
|
+
sc = next;
|
|
623
|
+
s++;
|
|
624
|
+
if (sc - save_col > 5) break;
|
|
625
|
+
} else {
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
int i = sc - save_col;
|
|
630
|
+
int padding;
|
|
631
|
+
const char *content;
|
|
632
|
+
int content_col;
|
|
633
|
+
if (i >= 5 || i < 1 || *s == '\0' || *s == '\n' || *s == '\r') {
|
|
634
|
+
padding = marker_len + 1;
|
|
635
|
+
content_col = after_col;
|
|
636
|
+
content = after;
|
|
637
|
+
if (i > 0) content = critic_advance_columns(after, &content_col, 1);
|
|
638
|
+
} else {
|
|
639
|
+
padding = marker_len + i;
|
|
640
|
+
content = s;
|
|
641
|
+
content_col = sc;
|
|
642
|
+
}
|
|
643
|
+
if (st->depth < 32) {
|
|
644
|
+
st->stack[st->depth].kind = CRITIC_BOX_ITEM;
|
|
645
|
+
st->stack[st->depth].need = marker_offset + padding;
|
|
646
|
+
st->stack[st->depth].has_content = false;
|
|
647
|
+
st->depth++;
|
|
648
|
+
}
|
|
649
|
+
p = content;
|
|
650
|
+
col = content_col;
|
|
651
|
+
st->in_paragraph = false;
|
|
652
|
+
st->in_code = false;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (critic_is_fence_opener(q) || critic_is_atx(q) || critic_is_rule(q, st->in_paragraph)) {
|
|
656
|
+
st->in_paragraph = false;
|
|
657
|
+
st->in_code = false;
|
|
658
|
+
critic_mark_content(st);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
st->in_paragraph = true;
|
|
662
|
+
st->in_code = false;
|
|
663
|
+
critic_mark_content(st);
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* True when this line is indented code and should be copied unchanged.
|
|
669
|
+
* Updates block state either way. Call only at the start of a line. */
|
|
670
|
+
static bool critic_indented_code_line(const char *line, critic_blocks *st) {
|
|
671
|
+
const char *p;
|
|
672
|
+
int col;
|
|
673
|
+
int matched = critic_match_boxes(line, st, &p, &col);
|
|
674
|
+
bool blank = critic_line_blank(line);
|
|
675
|
+
|
|
676
|
+
if (matched < st->depth) {
|
|
677
|
+
if (st->in_paragraph && !blank && !critic_interrupts_paragraph(p, col)) {
|
|
678
|
+
return false; /* lazy continuation */
|
|
679
|
+
}
|
|
680
|
+
st->depth = matched;
|
|
681
|
+
st->in_code = false;
|
|
682
|
+
st->in_paragraph = false;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (blank) {
|
|
686
|
+
st->in_paragraph = false;
|
|
687
|
+
if (matched < st->depth) st->in_code = false;
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (st->in_code) {
|
|
692
|
+
int indent = 0;
|
|
693
|
+
int c = col;
|
|
694
|
+
critic_skip_indent(p, &c, &indent);
|
|
695
|
+
if (indent >= 4) return true;
|
|
696
|
+
st->in_code = false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return critic_interpret_line(st, p, col);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* remaining counts bytes available before the terminating NUL. */
|
|
703
|
+
static bool critic_append(char **buf, char **write, size_t *remaining,
|
|
704
|
+
const char *src, size_t len) {
|
|
705
|
+
if (len >= *remaining) {
|
|
706
|
+
size_t written = (size_t)(*write - *buf);
|
|
707
|
+
size_t new_cap = (written + len + 1) * 2;
|
|
708
|
+
char *grown = realloc(*buf, new_cap);
|
|
709
|
+
if (!grown) return false;
|
|
710
|
+
*buf = grown;
|
|
711
|
+
*write = grown + written;
|
|
712
|
+
*remaining = new_cap - written - 1;
|
|
713
|
+
}
|
|
714
|
+
if (len) {
|
|
715
|
+
memcpy(*write, src, len);
|
|
716
|
+
*write += len;
|
|
717
|
+
*remaining -= len;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
|
|
281
722
|
/**
|
|
282
723
|
* Process Critic Markup in raw text (preprocessing approach)
|
|
283
|
-
* This is better than postprocessing because it avoids smart typography interference
|
|
724
|
+
* This is better than postprocessing because it avoids smart typography interference.
|
|
725
|
+
* Code spans, fenced blocks, and indented code blocks are copied through
|
|
726
|
+
* so the syntax can be shown literally.
|
|
284
727
|
*/
|
|
285
728
|
char *apex_process_critic_markup_text(const char *text, critic_mode_t mode) {
|
|
286
729
|
if (!text) return NULL;
|
|
287
730
|
|
|
288
731
|
size_t len = strlen(text);
|
|
289
|
-
size_t output_capacity = len * 2
|
|
732
|
+
size_t output_capacity = len * 2 + 1;
|
|
290
733
|
char *output = malloc(output_capacity);
|
|
291
734
|
if (!output) return NULL;
|
|
292
735
|
|
|
293
736
|
const char *read_pos = text;
|
|
294
737
|
char *write_pos = output;
|
|
295
|
-
size_t remaining_capacity = output_capacity;
|
|
738
|
+
size_t remaining_capacity = output_capacity - 1;
|
|
739
|
+
critic_blocks blocks;
|
|
740
|
+
memset(&blocks, 0, sizeof(blocks));
|
|
296
741
|
|
|
297
742
|
while (*read_pos) {
|
|
743
|
+
bool line_start = (read_pos == text || read_pos[-1] == '\n' ||
|
|
744
|
+
(read_pos[-1] == '\r' && *read_pos != '\n'));
|
|
745
|
+
if (line_start && critic_indented_code_line(read_pos, &blocks)) {
|
|
746
|
+
const char *next = critic_next_line(read_pos);
|
|
747
|
+
if (!critic_append(&output, &write_pos, &remaining_capacity, read_pos, (size_t)(next - read_pos))) {
|
|
748
|
+
free(output);
|
|
749
|
+
return NULL;
|
|
750
|
+
}
|
|
751
|
+
read_pos = next;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
size_t code_len = critic_code_region_len(text, read_pos);
|
|
756
|
+
if (code_len > 0) {
|
|
757
|
+
if (!critic_append(&output, &write_pos, &remaining_capacity, read_pos, code_len)) {
|
|
758
|
+
free(output);
|
|
759
|
+
return NULL;
|
|
760
|
+
}
|
|
761
|
+
read_pos += code_len;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
|
|
298
765
|
/* Look for { */
|
|
299
766
|
if (*read_pos == '{') {
|
|
300
767
|
int consumed;
|
|
@@ -310,11 +777,10 @@ char *apex_process_critic_markup_text(const char *text, critic_mode_t mode) {
|
|
|
310
777
|
/* Found valid critic markup - convert to HTML */
|
|
311
778
|
char *html = critic_to_html(type, content, content_len, old_text, old_len, mode);
|
|
312
779
|
if (html) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
remaining_capacity -= html_len;
|
|
780
|
+
if (!critic_append(&output, &write_pos, &remaining_capacity, html, strlen(html))) {
|
|
781
|
+
free(html);
|
|
782
|
+
free(output);
|
|
783
|
+
return NULL;
|
|
318
784
|
}
|
|
319
785
|
free(html);
|
|
320
786
|
}
|
|
@@ -324,9 +790,9 @@ char *apex_process_critic_markup_text(const char *text, critic_mode_t mode) {
|
|
|
324
790
|
}
|
|
325
791
|
|
|
326
792
|
/* Not critic markup, copy character */
|
|
327
|
-
if (remaining_capacity
|
|
328
|
-
|
|
329
|
-
|
|
793
|
+
if (!critic_append(&output, &write_pos, &remaining_capacity, read_pos, 1)) {
|
|
794
|
+
free(output);
|
|
795
|
+
return NULL;
|
|
330
796
|
}
|
|
331
797
|
read_pos++;
|
|
332
798
|
}
|
|
@@ -993,6 +993,17 @@ static bool extract_ial_from_paragraph(cmark_node *para, apex_attributes **attrs
|
|
|
993
993
|
return extract_ial_from_text(text, attrs_out, alds);
|
|
994
994
|
}
|
|
995
995
|
|
|
996
|
+
/* Drop a softbreak or hard break that only separated a removed IAL. */
|
|
997
|
+
static void ial_drop_break_before(cmark_node *node) {
|
|
998
|
+
cmark_node *prev = cmark_node_previous(node);
|
|
999
|
+
if (!prev) return;
|
|
1000
|
+
cmark_node_type prev_type = cmark_node_get_type(prev);
|
|
1001
|
+
if (prev_type == CMARK_NODE_SOFTBREAK || prev_type == CMARK_NODE_LINEBREAK) {
|
|
1002
|
+
cmark_node_unlink(prev);
|
|
1003
|
+
cmark_node_free(prev);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
996
1007
|
/**
|
|
997
1008
|
* Handle span-level IAL (inline elements with attributes)
|
|
998
1009
|
* Example: [Link](url){: .class} or {: #id}
|
|
@@ -1210,7 +1221,8 @@ static bool process_span_ial_in_container(cmark_node *container, ald_entry *alds
|
|
|
1210
1221
|
* Content before IAL can be: a previous sibling (e.g. text + softbreak + IAL text node), or text before IAL in this node (e.g. single node "Text\n{: .lead }"). */
|
|
1211
1222
|
bool has_preceding_content = (cmark_node_previous(child) != NULL) || (ial_start > text);
|
|
1212
1223
|
if (container_type == CMARK_NODE_PARAGRAPH && has_preceding_content) {
|
|
1213
|
-
/* IAL was at end of text - apply to paragraph (e.g. "Text\n{: .lead }" on one block)
|
|
1224
|
+
/* IAL was at end of text - apply to this paragraph (e.g. "Text\n{: .lead }" on one block).
|
|
1225
|
+
* Inside a blockquote, "> {: .class}" follows the paragraph, so the class stays on that paragraph. */
|
|
1214
1226
|
char *attr_str = attributes_to_html(attrs);
|
|
1215
1227
|
cmark_node_set_user_data(container, attr_str);
|
|
1216
1228
|
apex_free_attributes(attrs);
|
|
@@ -1235,6 +1247,7 @@ static bool process_span_ial_in_container(cmark_node *container, ald_entry *alds
|
|
|
1235
1247
|
while (end >= new_text && isspace((unsigned char)*end)) *end-- = '\0';
|
|
1236
1248
|
}
|
|
1237
1249
|
if (strlen(new_text) == 0) {
|
|
1250
|
+
ial_drop_break_before(child);
|
|
1238
1251
|
cmark_node_unlink(child);
|
|
1239
1252
|
cmark_node_free(child);
|
|
1240
1253
|
free(new_text);
|
|
@@ -1244,6 +1257,7 @@ static bool process_span_ial_in_container(cmark_node *container, ald_entry *alds
|
|
|
1244
1257
|
}
|
|
1245
1258
|
}
|
|
1246
1259
|
} else {
|
|
1260
|
+
ial_drop_break_before(child);
|
|
1247
1261
|
cmark_node_unlink(child);
|
|
1248
1262
|
cmark_node_free(child);
|
|
1249
1263
|
}
|
|
@@ -1526,6 +1540,138 @@ static bool is_pure_ial_paragraph(cmark_node *para) {
|
|
|
1526
1540
|
* Returns the node to free (if any), or NULL
|
|
1527
1541
|
* Caller must free the returned node after iteration is complete
|
|
1528
1542
|
*/
|
|
1543
|
+
/* Copy attrs without one key="value" attribute. */
|
|
1544
|
+
static char *ial_attrs_without(const char *attrs, const char *name) {
|
|
1545
|
+
size_t nlen = strlen(name);
|
|
1546
|
+
char *out = malloc(strlen(attrs) + 1);
|
|
1547
|
+
if (!out) return NULL;
|
|
1548
|
+
char *write = out;
|
|
1549
|
+
const char *p = attrs;
|
|
1550
|
+
while (*p) {
|
|
1551
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
1552
|
+
if (!*p) break;
|
|
1553
|
+
const char *start = p;
|
|
1554
|
+
bool drop = strncmp(p, name, nlen) == 0 && p[nlen] == '=' &&
|
|
1555
|
+
(p[nlen + 1] == '"' || p[nlen + 1] == '\'');
|
|
1556
|
+
if (drop) {
|
|
1557
|
+
char quote = p[nlen + 1];
|
|
1558
|
+
p += nlen + 2;
|
|
1559
|
+
while (*p && *p != quote) p++;
|
|
1560
|
+
if (*p == quote) p++;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
while (*p && !isspace((unsigned char)*p)) p++;
|
|
1564
|
+
if (write != out) *write++ = ' ';
|
|
1565
|
+
memcpy(write, start, (size_t)(p - start));
|
|
1566
|
+
write += p - start;
|
|
1567
|
+
}
|
|
1568
|
+
*write = '\0';
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/* True when an HTML block is only a closing tag (and comments), so a
|
|
1573
|
+
* following IAL still belongs to the block before it. */
|
|
1574
|
+
static bool ial_html_is_closer_only(cmark_node *node) {
|
|
1575
|
+
const char *html = cmark_node_get_literal(node);
|
|
1576
|
+
if (!html) return false;
|
|
1577
|
+
for (const char *p = html; *p; p++) {
|
|
1578
|
+
if (*p == '<' && p[1] && p[1] != '/' && p[1] != '!' && p[1] != '?') {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/* Insert IAL attributes into the first opening tag of an HTML block.
|
|
1586
|
+
* An existing class is extended instead of adding a second class attribute. */
|
|
1587
|
+
static char *ial_html_with_attrs(const char *html, const char *attrs) {
|
|
1588
|
+
if (!html || !attrs || !*attrs) return NULL;
|
|
1589
|
+
|
|
1590
|
+
const char *tag = NULL;
|
|
1591
|
+
for (const char *p = html; *p; p++) {
|
|
1592
|
+
if (*p == '<' && p[1] && p[1] != '/' && p[1] != '!' && p[1] != '?') {
|
|
1593
|
+
tag = p;
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (!tag) return NULL;
|
|
1598
|
+
const char *gt = strchr(tag, '>');
|
|
1599
|
+
if (!gt) return NULL;
|
|
1600
|
+
|
|
1601
|
+
const char *class_at = NULL;
|
|
1602
|
+
const char *class_val = NULL;
|
|
1603
|
+
const char *class_end = NULL;
|
|
1604
|
+
for (const char *p = tag + 1; p + 7 < gt; p++) {
|
|
1605
|
+
if ((p == tag + 1 || isspace((unsigned char)p[-1])) && strncmp(p, "class=\"", 7) == 0) {
|
|
1606
|
+
class_at = p;
|
|
1607
|
+
class_val = p + 7;
|
|
1608
|
+
class_end = memchr(class_val, '"', (size_t)(gt - class_val));
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const char *ial_class = NULL;
|
|
1614
|
+
const char *ial_class_end = NULL;
|
|
1615
|
+
const char *ial_at = strstr(attrs, "class=\"");
|
|
1616
|
+
if (ial_at && (ial_at == attrs || isspace((unsigned char)ial_at[-1]))) {
|
|
1617
|
+
ial_class = ial_at + 7;
|
|
1618
|
+
ial_class_end = strchr(ial_class, '"');
|
|
1619
|
+
if (!ial_class_end) ial_class = NULL;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
char *extra = NULL;
|
|
1623
|
+
char *merged_class = NULL;
|
|
1624
|
+
if (class_at && class_end && ial_class) {
|
|
1625
|
+
size_t old_len = (size_t)(class_end - class_val);
|
|
1626
|
+
size_t add_len = (size_t)(ial_class_end - ial_class);
|
|
1627
|
+
merged_class = malloc(old_len + add_len + 2);
|
|
1628
|
+
if (!merged_class) return NULL;
|
|
1629
|
+
memcpy(merged_class, class_val, old_len);
|
|
1630
|
+
merged_class[old_len] = ' ';
|
|
1631
|
+
memcpy(merged_class + old_len + 1, ial_class, add_len);
|
|
1632
|
+
merged_class[old_len + 1 + add_len] = '\0';
|
|
1633
|
+
extra = ial_attrs_without(attrs, "class");
|
|
1634
|
+
} else {
|
|
1635
|
+
extra = strdup(attrs);
|
|
1636
|
+
}
|
|
1637
|
+
if (!extra) {
|
|
1638
|
+
free(merged_class);
|
|
1639
|
+
return NULL;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
size_t html_len = strlen(html);
|
|
1643
|
+
size_t extra_len = strlen(extra);
|
|
1644
|
+
size_t merged_len = merged_class ? strlen(merged_class) : 0;
|
|
1645
|
+
char *out = malloc(html_len + extra_len + merged_len + 2);
|
|
1646
|
+
if (!out) {
|
|
1647
|
+
free(extra);
|
|
1648
|
+
free(merged_class);
|
|
1649
|
+
return NULL;
|
|
1650
|
+
}
|
|
1651
|
+
char *write = out;
|
|
1652
|
+
if (merged_class && class_val && class_end) {
|
|
1653
|
+
memcpy(write, html, (size_t)(class_val - html));
|
|
1654
|
+
write += class_val - html;
|
|
1655
|
+
memcpy(write, merged_class, merged_len);
|
|
1656
|
+
write += merged_len;
|
|
1657
|
+
memcpy(write, class_end, (size_t)(gt - class_end));
|
|
1658
|
+
write += gt - class_end;
|
|
1659
|
+
} else {
|
|
1660
|
+
memcpy(write, html, (size_t)(gt - html));
|
|
1661
|
+
write += gt - html;
|
|
1662
|
+
}
|
|
1663
|
+
if (extra_len > 0) {
|
|
1664
|
+
*write++ = ' ';
|
|
1665
|
+
memcpy(write, extra, extra_len);
|
|
1666
|
+
write += extra_len;
|
|
1667
|
+
}
|
|
1668
|
+
memcpy(write, gt, html_len - (size_t)(gt - html) + 1);
|
|
1669
|
+
|
|
1670
|
+
free(extra);
|
|
1671
|
+
free(merged_class);
|
|
1672
|
+
return out;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1529
1675
|
static cmark_node *process_node_ial(cmark_node *node, ald_entry *alds) {
|
|
1530
1676
|
if (!node) return NULL;
|
|
1531
1677
|
|
|
@@ -1570,30 +1716,34 @@ static cmark_node *process_node_ial(cmark_node *node, ald_entry *alds) {
|
|
|
1570
1716
|
/* No span IAL found, fall through to check for next-line IAL */
|
|
1571
1717
|
}
|
|
1572
1718
|
|
|
1573
|
-
/*
|
|
1719
|
+
/* Any block can take an IAL on the following line. */
|
|
1574
1720
|
if (type != CMARK_NODE_HEADING &&
|
|
1575
1721
|
type != CMARK_NODE_PARAGRAPH &&
|
|
1576
1722
|
type != CMARK_NODE_BLOCK_QUOTE &&
|
|
1577
1723
|
type != CMARK_NODE_CODE_BLOCK &&
|
|
1578
1724
|
type != CMARK_NODE_LIST &&
|
|
1579
1725
|
type != CMARK_NODE_ITEM &&
|
|
1580
|
-
type != CMARK_NODE_TABLE
|
|
1726
|
+
type != CMARK_NODE_TABLE &&
|
|
1727
|
+
type != CMARK_NODE_THEMATIC_BREAK &&
|
|
1728
|
+
type != CMARK_NODE_HTML_BLOCK) {
|
|
1581
1729
|
return NULL; /* No node to free */
|
|
1582
1730
|
}
|
|
1583
1731
|
|
|
1584
1732
|
|
|
1585
|
-
/*
|
|
1733
|
+
/* A pure IAL paragraph is the marker, not a block that takes another IAL. */
|
|
1734
|
+
if (type == CMARK_NODE_PARAGRAPH && is_pure_ial_paragraph(node)) {
|
|
1735
|
+
return NULL;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/* Look at the next sibling for an IAL paragraph. A closing-tag HTML
|
|
1739
|
+
* block can sit between a paragraph and its IAL. */
|
|
1586
1740
|
cmark_node *next = cmark_node_next(node);
|
|
1587
1741
|
if (!next) {
|
|
1588
1742
|
return NULL; /* No node to free */
|
|
1589
1743
|
}
|
|
1590
1744
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
cmark_node_type next_type = cmark_node_get_type(next);
|
|
1594
|
-
if (next_type != CMARK_NODE_HTML_BLOCK && next_type != CMARK_NODE_CUSTOM_BLOCK) {
|
|
1595
|
-
break;
|
|
1596
|
-
}
|
|
1745
|
+
while (next && cmark_node_get_type(next) == CMARK_NODE_HTML_BLOCK &&
|
|
1746
|
+
ial_html_is_closer_only(next)) {
|
|
1597
1747
|
next = cmark_node_next(next);
|
|
1598
1748
|
}
|
|
1599
1749
|
|
|
@@ -1605,10 +1755,18 @@ static cmark_node *process_node_ial(cmark_node *node, ald_entry *alds) {
|
|
|
1605
1755
|
if (is_pure_ial_paragraph(next)) {
|
|
1606
1756
|
apex_attributes *attrs = NULL;
|
|
1607
1757
|
if (extract_ial_from_paragraph(next, &attrs, alds)) {
|
|
1608
|
-
/* Store attributes in this node */
|
|
1609
1758
|
char *attr_str = attributes_to_html(attrs);
|
|
1610
|
-
cmark_node_set_user_data(node, attr_str);
|
|
1611
1759
|
apex_free_attributes(attrs);
|
|
1760
|
+
if (type == CMARK_NODE_HTML_BLOCK) {
|
|
1761
|
+
const char *literal = cmark_node_get_literal(node);
|
|
1762
|
+
char *merged = ial_html_with_attrs(literal, attr_str);
|
|
1763
|
+
free(attr_str);
|
|
1764
|
+
if (!merged) return NULL;
|
|
1765
|
+
cmark_node_set_literal(node, merged);
|
|
1766
|
+
free(merged);
|
|
1767
|
+
} else {
|
|
1768
|
+
cmark_node_set_user_data(node, attr_str);
|
|
1769
|
+
}
|
|
1612
1770
|
|
|
1613
1771
|
/* Return node to be unlinked and freed after iteration completes */
|
|
1614
1772
|
/* Don't unlink here - that invalidates the iterator */
|
|
@@ -1642,6 +1800,14 @@ void apex_process_ial_in_tree(cmark_node *node, ald_entry *alds) {
|
|
|
1642
1800
|
/* Process node and collect any nodes that need to be freed */
|
|
1643
1801
|
cmark_node *node_to_free = process_node_ial(cur, alds);
|
|
1644
1802
|
if (node_to_free) {
|
|
1803
|
+
bool already_queued = false;
|
|
1804
|
+
for (size_t i = 0; i < free_count; i++) {
|
|
1805
|
+
if (nodes_to_free[i] == node_to_free) {
|
|
1806
|
+
already_queued = true;
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (already_queued) continue;
|
|
1645
1811
|
/* Expand array if needed */
|
|
1646
1812
|
if (free_count >= free_capacity) {
|
|
1647
1813
|
free_capacity = free_capacity ? free_capacity * 2 : 16;
|
|
@@ -2908,6 +3074,42 @@ static bool ial_append_verbatim(char **buf, char **write, size_t *remaining,
|
|
|
2908
3074
|
/**
|
|
2909
3075
|
* Preprocess markdown to extract image attributes and URL-encode all link URLs
|
|
2910
3076
|
*/
|
|
3077
|
+
/* Closing ')' of a link or image destination.
|
|
3078
|
+
* Quotes hide parentheses, and unquoted '(' must be balanced, so a title
|
|
3079
|
+
* like "Output options (Markdown)" does not end the destination early. */
|
|
3080
|
+
static const char *ial_destination_closer(const char *start) {
|
|
3081
|
+
int depth = 1;
|
|
3082
|
+
bool in_quote = false;
|
|
3083
|
+
char quote = 0;
|
|
3084
|
+
const char *p = start;
|
|
3085
|
+
|
|
3086
|
+
if (!start) return NULL;
|
|
3087
|
+
while (*p && *p != '\n' && *p != '\r') {
|
|
3088
|
+
if (in_quote) {
|
|
3089
|
+
if (*p == '\\' && p[1]) {
|
|
3090
|
+
p += 2;
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
if (*p == quote) in_quote = false;
|
|
3094
|
+
p++;
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
if (*p == '"' || *p == '\'') {
|
|
3098
|
+
in_quote = true;
|
|
3099
|
+
quote = *p++;
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (*p == '(') {
|
|
3103
|
+
depth++;
|
|
3104
|
+
} else if (*p == ')') {
|
|
3105
|
+
depth--;
|
|
3106
|
+
if (depth == 0) return p;
|
|
3107
|
+
}
|
|
3108
|
+
p++;
|
|
3109
|
+
}
|
|
3110
|
+
return NULL;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
2911
3113
|
char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_attrs, apex_mode_t mode) {
|
|
2912
3114
|
if (!text) return NULL;
|
|
2913
3115
|
|
|
@@ -2976,12 +3178,10 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
2976
3178
|
const char *attr_start = NULL;
|
|
2977
3179
|
const char *paren_end = NULL;
|
|
2978
3180
|
|
|
2979
|
-
/* Find the closing paren first
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
if (
|
|
2983
|
-
paren_end = p;
|
|
2984
|
-
} else {
|
|
3181
|
+
/* Find the closing paren first. Parentheses inside a quoted
|
|
3182
|
+
* title, and balanced parentheses in the URL, do not count. */
|
|
3183
|
+
paren_end = ial_destination_closer(url_start);
|
|
3184
|
+
if (!paren_end) {
|
|
2985
3185
|
/* Malformed - skip just the ! and continue */
|
|
2986
3186
|
read = img_start + 1;
|
|
2987
3187
|
continue;
|
|
@@ -4064,12 +4264,13 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
4064
4264
|
const char *url_end = NULL;
|
|
4065
4265
|
const char *paren_end = NULL;
|
|
4066
4266
|
|
|
4067
|
-
/* Find the closing paren first
|
|
4068
|
-
|
|
4069
|
-
|
|
4267
|
+
/* Find the closing paren first. A quoted title may contain
|
|
4268
|
+
* parentheses; stop at a newline when the destination never closes. */
|
|
4269
|
+
paren_end = ial_destination_closer(url_start);
|
|
4270
|
+
if (!paren_end) {
|
|
4271
|
+
p = url_start;
|
|
4272
|
+
while (*p && *p != '\n' && *p != '\r') p++;
|
|
4070
4273
|
paren_end = p;
|
|
4071
|
-
} else {
|
|
4072
|
-
paren_end = p; /* End at newline or end of string */
|
|
4073
4274
|
}
|
|
4074
4275
|
|
|
4075
4276
|
/* Scan forward looking for titles: "title", 'title', or (title) */
|
|
@@ -4483,10 +4684,9 @@ char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_
|
|
|
4483
4684
|
const char *attr_start = NULL;
|
|
4484
4685
|
const char *paren_end = NULL;
|
|
4485
4686
|
|
|
4486
|
-
/* Find closing paren */
|
|
4487
|
-
|
|
4488
|
-
if (
|
|
4489
|
-
paren_end = p;
|
|
4687
|
+
/* Find closing paren, skipping parentheses inside quotes. */
|
|
4688
|
+
paren_end = ial_destination_closer(url_start);
|
|
4689
|
+
if (paren_end) {
|
|
4490
4690
|
p = url_start;
|
|
4491
4691
|
|
|
4492
4692
|
/* Look for attributes */
|
|
@@ -350,6 +350,7 @@ typedef struct {
|
|
|
350
350
|
int strong_count;
|
|
351
351
|
int emph_count;
|
|
352
352
|
int code_inline_count;
|
|
353
|
+
int hr_count;
|
|
353
354
|
} element_counters;
|
|
354
355
|
|
|
355
356
|
/**
|
|
@@ -482,6 +483,7 @@ static void collect_nodes_with_attrs_recursive(cmark_node *node, attr_node **lis
|
|
|
482
483
|
else if (type == CMARK_NODE_LIST) elem_idx = counters->list_count++;
|
|
483
484
|
else if (type == CMARK_NODE_ITEM) elem_idx = counters->item_count++;
|
|
484
485
|
else if (type == CMARK_NODE_CODE_BLOCK) elem_idx = counters->code_count++;
|
|
486
|
+
else if (type == CMARK_NODE_THEMATIC_BREAK) elem_idx = counters->hr_count++;
|
|
485
487
|
/* Inline elements need indices too - each type has its own counter */
|
|
486
488
|
else if (type == CMARK_NODE_LINK) elem_idx = counters->link_count++;
|
|
487
489
|
else if (type == CMARK_NODE_IMAGE) elem_idx = counters->image_count++;
|
|
@@ -623,6 +625,9 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
|
|
|
623
625
|
} else if (tag_len == 2 && memcmp(tag_start, "li", 2) == 0) {
|
|
624
626
|
elem_type = CMARK_NODE_ITEM;
|
|
625
627
|
elem_idx = html_counters.item_count++;
|
|
628
|
+
} else if (tag_len == 2 && memcmp(tag_start, "hr", 2) == 0) {
|
|
629
|
+
elem_type = CMARK_NODE_THEMATIC_BREAK;
|
|
630
|
+
elem_idx = html_counters.hr_count++;
|
|
626
631
|
} else if (tag_len == 3 && memcmp(tag_start, "pre", 3) == 0) {
|
|
627
632
|
elem_type = CMARK_NODE_CODE_BLOCK;
|
|
628
633
|
elem_idx = html_counters.code_count++;
|
|
@@ -776,13 +781,19 @@ char *apex_render_html_with_attributes(cmark_node *document, int options) {
|
|
|
776
781
|
}
|
|
777
782
|
html_fingerprint[fp_idx] = '\0';
|
|
778
783
|
} else {
|
|
779
|
-
/* For block elements, extract text content
|
|
784
|
+
/* For block elements, extract text content.
|
|
785
|
+
* Paragraph attributes are fingerprinted from the first text
|
|
786
|
+
* node, which ends at a softbreak, so stop at the first newline. */
|
|
780
787
|
const char *content_start = tag_name_end;
|
|
781
788
|
while (*content_start && *content_start != '>') content_start++;
|
|
782
789
|
if (*content_start == '>') content_start++;
|
|
783
790
|
|
|
784
791
|
const char *text_p = content_start;
|
|
785
792
|
while (*text_p && *text_p != '<' && fp_idx < 50) {
|
|
793
|
+
if (elem_type == CMARK_NODE_PARAGRAPH &&
|
|
794
|
+
(*text_p == '\n' || *text_p == '\r')) {
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
786
797
|
html_fingerprint[fp_idx++] = *text_p++;
|
|
787
798
|
}
|
|
788
799
|
html_fingerprint[fp_idx] = '\0';
|
data/lib/apex/version.rb
CHANGED