apex-ruby 1.0.39 → 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: f037626c97e2d9d1da593da4e5cfe66b74bac805d7bad91e8f5547ff676cb394
4
- data.tar.gz: a1ef5604a103f328a970246606c4e9c79b2ea6ffece769532250da7499dc287d
3
+ metadata.gz: ee084bd77c91c213aa431c0485b23ec3a15106ee3f341d92e5885f29c8351c93
4
+ data.tar.gz: 7840707f775f33a7562734f60e5d69db520a93d98e95d9450483e684972d01e6
5
5
  SHA512:
6
- metadata.gz: b6f994047113301b183aa7529089d6342f6847ace386ceb281cd80675b5ef79afc6a7c423318357533c03102b24916e8b7e20405c750b903189674a2cb321380
7
- data.tar.gz: 90101aea55503ce808d9cc4f6018c466ff9a03791deac096f4e13e85858536beb5abe6dabc7d90e89a0f2fe91b7fde7bdd9d054bc841d99b54fd45679b98d344
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 41
25
- #define APEX_VERSION_STRING "1.1.41"
24
+ #define APEX_VERSION_PATCH 43
25
+ #define APEX_VERSION_STRING "1.1.43"
26
26
 
27
27
  /**
28
28
  * Processor compatibility modes
@@ -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 ![Image](img){: #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
- /* Only certain block types can have IAL after them */
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) { /* Tables can have IAL */
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
- /* Look at next sibling(s) for IAL paragraph (skip over HTML blocks; cmark may put </div> between paragraph and IAL) */
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
- /* Skip HTML/custom blocks so we find a following IAL paragraph (e.g. paragraph, </div> html_block, {: .lead} paragraph) */
1592
- while (next) {
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
- p = url_start;
2981
- while (*p && *p != ')' && *p != '\n') p++;
2982
- if (*p == ')') {
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
- while (*p && *p != ')' && *p != '\n') p++;
4069
- if (*p == ')') {
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
- while (*p && *p != ')' && *p != '\n') p++;
4488
- if (*p == ')') {
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
@@ -6,7 +6,7 @@ module Apex
6
6
  # via Apex::Native.version.
7
7
  module Version
8
8
  # Current gem version.
9
- VERSION = "1.0.39"
9
+ VERSION = "1.0.40"
10
10
  end
11
11
 
12
12
  # Shortcut to gem version string.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apex-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.39
4
+ version: 1.0.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra