commonmarker 2.9.0 → 2.10.1

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.
@@ -1,3 +1,4 @@
1
+ use crate::rctree::Node;
1
2
  use comrak::arena_tree::Node as ComrakNode;
2
3
  use comrak::nodes::{
3
4
  AlertType, Ast as ComrakAst, AstNode as ComrakAstNode, ListDelimType, ListType, NodeAlert,
@@ -8,7 +9,6 @@ use comrak::nodes::{
8
9
  };
9
10
  use magnus::{function, method, scan_args, Module, Object, RHash, RModule, Symbol, Value};
10
11
  use magnus::{RArray, Ruby};
11
- use rctree::Node;
12
12
  use typed_arena::Arena;
13
13
 
14
14
  use std::cell::RefCell;
@@ -31,6 +31,39 @@ pub struct CommonmarkerNode {
31
31
  /// SAFETY: This is safe because we only access this data when the GVL is held.
32
32
  unsafe impl Send for CommonmarkerNode {}
33
33
 
34
+ /// Reports whether a node value supports a given property, where "property" is
35
+ /// a method name with any trailing `=` or `?` removed.
36
+ ///
37
+ /// `None` means the property is not type-dependent at all
38
+ /// (`walk`, `type`, `delete`, ...).
39
+ fn value_supports(value: &ComrakNodeValue, property: &str) -> Option<bool> {
40
+ let supported = match property {
41
+ "string_content" => matches!(
42
+ value,
43
+ ComrakNodeValue::Text(_) | ComrakNodeValue::Code(_) | ComrakNodeValue::CodeBlock(_)
44
+ ),
45
+ "literal" => matches!(
46
+ value,
47
+ ComrakNodeValue::Text(_)
48
+ | ComrakNodeValue::Code(_)
49
+ | ComrakNodeValue::CodeBlock(_)
50
+ | ComrakNodeValue::HtmlBlock(_)
51
+ | ComrakNodeValue::HtmlInline(_)
52
+ | ComrakNodeValue::Raw(_)
53
+ | ComrakNodeValue::Math(_)
54
+ | ComrakNodeValue::FrontMatter(_)
55
+ ),
56
+ "url" | "title" => matches!(value, ComrakNodeValue::Link(_) | ComrakNodeValue::Image(_)),
57
+ "header_level" => matches!(value, ComrakNodeValue::Heading(_)),
58
+ "list_type" | "list_start" | "list_tight" => matches!(value, ComrakNodeValue::List(_)),
59
+ "fenced" | "fence_info" => matches!(value, ComrakNodeValue::CodeBlock(_)),
60
+ "alert_type" => matches!(value, ComrakNodeValue::Alert(_)),
61
+ _ => return None,
62
+ };
63
+
64
+ Some(supported)
65
+ }
66
+
34
67
  impl CommonmarkerNode {
35
68
  pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, magnus::Error> {
36
69
  let args = scan_args::scan_args::<_, (), (), (), _, ()>(args)?;
@@ -566,42 +599,50 @@ impl CommonmarkerNode {
566
599
  })
567
600
  }
568
601
 
602
+ /// Copies a comrak tree into a Ruby-visible tree, failing if any node sits
603
+ /// more than `max_nesting_depth` levels below the root.
569
604
  pub fn new_from_comrak_node<'a>(
605
+ ruby: &Ruby,
570
606
  comrak_root_node: &'a ComrakAstNode<'a>,
607
+ max_nesting_depth: usize,
571
608
  ) -> Result<CommonmarkerNode, magnus::Error> {
572
- let comrak_ast = comrak_root_node.data.clone().into_inner();
573
-
574
- fn iter_nodes<'a>(comrak_node: &'a ComrakAstNode<'a>) -> CommonmarkerNode {
575
- let comrak_node_ast = comrak_node.data.clone().into_inner();
576
- let commonmark_node = CommonmarkerNode {
577
- inner: Node::new(CommonmarkerAst {
578
- data: comrak_node_ast,
579
- }),
609
+ let commonmarker_root_node = Node::new(CommonmarkerAst {
610
+ data: comrak_root_node.data.borrow().clone(),
611
+ });
612
+
613
+ // Depth-first walk with an explicit stack instead of recursion, so that
614
+ // deeply nested input (e.g. thousands of nested blockquotes) cannot
615
+ // overflow the native stack. Each entry pairs an iterator over a comrak
616
+ // node's remaining children with the copy of that node the children get
617
+ // appended to.
618
+ let mut stack = vec![(comrak_root_node.children(), commonmarker_root_node.clone())];
619
+
620
+ loop {
621
+ // The stack holds one entry per ancestor, so its length is the
622
+ // depth of the next child produced.
623
+ let depth = stack.len();
624
+ let Some((children, parent)) = stack.last_mut() else {
625
+ break;
626
+ };
627
+ let Some(comrak_child) = children.next() else {
628
+ stack.pop();
629
+ continue;
580
630
  };
581
631
 
582
- for c in comrak_node.children() {
583
- match commonmark_node.append_child_node(&iter_nodes(c)) {
584
- Ok(_) => {}
585
- Err(e) => {
586
- panic!("cannot append node: {}", e);
587
- }
588
- }
632
+ if depth > max_nesting_depth {
633
+ return Err(crate::nesting::exceeded_error(ruby, max_nesting_depth));
589
634
  }
590
635
 
591
- commonmark_node
592
- }
593
-
594
- let commonmarker_root_node = CommonmarkerNode {
595
- inner: Node::new(CommonmarkerAst { data: comrak_ast }),
596
- };
597
-
598
- for child in comrak_root_node.children() {
599
- let new_child = iter_nodes(child);
600
-
601
- commonmarker_root_node.append_child_node(&new_child)?;
636
+ let node = Node::new(CommonmarkerAst {
637
+ data: comrak_child.data.borrow().clone(),
638
+ });
639
+ parent.append(node.clone());
640
+ stack.push((comrak_child.children(), node));
602
641
  }
603
642
 
604
- Ok(commonmarker_root_node)
643
+ Ok(CommonmarkerNode {
644
+ inner: commonmarker_root_node,
645
+ })
605
646
  }
606
647
 
607
648
  fn type_to_symbol(ruby: &Ruby, rb_self: &Self) -> Symbol {
@@ -784,6 +825,63 @@ impl CommonmarkerNode {
784
825
  }
785
826
  }
786
827
 
828
+ /// Returns `true` or `false` if the given method is only available for
829
+ /// certain node types, and `nil` if its availability does not depend on
830
+ /// the node's type. Backs `Node#respond_to?`.
831
+ fn supports_method(rb_self: &Self, name: Symbol) -> Option<bool> {
832
+ let node = rb_self.inner.borrow();
833
+ let name = name.to_string();
834
+ let property = name.trim_end_matches('=').trim_end_matches('?');
835
+
836
+ value_supports(&node.data.value, property)
837
+ }
838
+
839
+ fn get_literal(ruby: &Ruby, rb_self: &Self) -> Result<String, magnus::Error> {
840
+ let node = rb_self.inner.borrow();
841
+
842
+ match &node.data.value {
843
+ ComrakNodeValue::Text(text) => Ok(text.to_string()),
844
+ ComrakNodeValue::Code(code) => Ok(code.literal.to_string()),
845
+ ComrakNodeValue::CodeBlock(code_block) => Ok(code_block.literal.to_string()),
846
+ ComrakNodeValue::HtmlBlock(html_block) => Ok(html_block.literal.to_string()),
847
+ ComrakNodeValue::HtmlInline(html_inline) => Ok(html_inline.to_string()),
848
+ ComrakNodeValue::Raw(raw) => Ok(raw.to_string()),
849
+ ComrakNodeValue::Math(math) => Ok(math.literal.to_string()),
850
+ ComrakNodeValue::FrontMatter(front_matter) => Ok(front_matter.to_string()),
851
+ _ => Err(magnus::Error::new(
852
+ ruby.exception_type_error(),
853
+ "node does not have a literal",
854
+ )),
855
+ }
856
+ }
857
+
858
+ fn set_literal(
859
+ ruby: &Ruby,
860
+ rb_self: &Self,
861
+ new_literal: String,
862
+ ) -> Result<bool, magnus::Error> {
863
+ let mut node = rb_self.inner.borrow_mut();
864
+
865
+ match node.data.value {
866
+ ComrakNodeValue::Text(ref mut text) => *text = new_literal.into(),
867
+ ComrakNodeValue::Code(ref mut code) => code.literal = new_literal,
868
+ ComrakNodeValue::CodeBlock(ref mut code_block) => code_block.literal = new_literal,
869
+ ComrakNodeValue::HtmlBlock(ref mut html_block) => html_block.literal = new_literal,
870
+ ComrakNodeValue::HtmlInline(ref mut html_inline) => *html_inline = new_literal,
871
+ ComrakNodeValue::Raw(ref mut raw) => *raw = new_literal,
872
+ ComrakNodeValue::Math(ref mut math) => math.literal = new_literal,
873
+ ComrakNodeValue::FrontMatter(ref mut front_matter) => *front_matter = new_literal,
874
+ _ => {
875
+ return Err(magnus::Error::new(
876
+ ruby.exception_type_error(),
877
+ "node does not have a literal",
878
+ ))
879
+ }
880
+ }
881
+
882
+ Ok(true)
883
+ }
884
+
787
885
  fn get_title(ruby: &Ruby, rb_self: &Self) -> Result<String, magnus::Error> {
788
886
  let node = rb_self.inner.borrow();
789
887
 
@@ -1037,6 +1135,39 @@ impl CommonmarkerNode {
1037
1135
  }
1038
1136
  }
1039
1137
 
1138
+ /// Copies this node and its descendants into `arena` as a comrak tree so that
1139
+ /// comrak's formatters can render it.
1140
+ ///
1141
+ /// Depth-first walk with an explicit stack instead of recursion, so that
1142
+ /// deeply nested input cannot overflow the native stack. Each entry pairs an
1143
+ /// iterator over a node's remaining children with the copy of that node the
1144
+ /// children get appended to.
1145
+ fn to_comrak_node<'a>(&self, arena: &'a Arena<ComrakAstNode<'a>>) -> &'a ComrakAstNode<'a> {
1146
+ let comrak_root_node: &'a ComrakAstNode<'a> = arena.alloc(ComrakNode::new(RefCell::new(
1147
+ self.inner.borrow().data.clone(),
1148
+ )));
1149
+
1150
+ let mut stack = vec![(self.inner.children(), comrak_root_node)];
1151
+
1152
+ loop {
1153
+ let Some((children, parent)) = stack.last_mut() else {
1154
+ break;
1155
+ };
1156
+ let parent: &'a ComrakAstNode<'a> = parent;
1157
+ let Some(child) = children.next() else {
1158
+ stack.pop();
1159
+ continue;
1160
+ };
1161
+
1162
+ let comrak_node: &'a ComrakAstNode<'a> =
1163
+ arena.alloc(ComrakNode::new(RefCell::new(child.borrow().data.clone())));
1164
+ parent.append(comrak_node);
1165
+ stack.push((child.children(), comrak_node));
1166
+ }
1167
+
1168
+ comrak_root_node
1169
+ }
1170
+
1040
1171
  fn to_html(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<String, magnus::Error> {
1041
1172
  let args = scan_args::scan_args::<(), (), (), (), _, ()>(args)?;
1042
1173
 
@@ -1081,42 +1212,18 @@ impl CommonmarkerNode {
1081
1212
  };
1082
1213
 
1083
1214
  match syntect_adapter {
1084
- Some(ref adapter) => comrak_plugins.render.codefence_syntax_highlighter = Some(adapter),
1215
+ Some(ref adapter) => {
1216
+ comrak_plugins.render.codefence_syntax_highlighter = Some(adapter.as_ref())
1217
+ }
1085
1218
  None => comrak_plugins.render.codefence_syntax_highlighter = None,
1086
1219
  }
1087
1220
 
1088
1221
  let arena: Arena<ComrakAstNode> = Arena::new();
1089
- fn iter_nodes<'a>(
1090
- arena: &'a Arena<comrak::arena_tree::Node<'a, RefCell<ComrakAst>>>,
1091
- node: &CommonmarkerNode,
1092
- ) -> &'a comrak::arena_tree::Node<'a, std::cell::RefCell<comrak::nodes::Ast>> {
1093
- let comrak_node: &'a mut ComrakAstNode = arena.alloc(ComrakNode::new(RefCell::new(
1094
- node.inner.borrow().data.clone(),
1095
- )));
1096
-
1097
- for c in node.inner.children() {
1098
- let child = CommonmarkerNode { inner: c };
1099
- let child_node = iter_nodes(arena, &child);
1100
- comrak_node.append(child_node);
1101
- }
1102
-
1103
- comrak_node
1104
- }
1105
-
1106
- let comrak_root_node: ComrakNode<RefCell<ComrakAst>> =
1107
- ComrakNode::new(RefCell::new(rb_self.inner.borrow().data.clone()));
1108
-
1109
- for c in rb_self.inner.children() {
1110
- let child = CommonmarkerNode { inner: c };
1111
-
1112
- let new_child = iter_nodes(&arena, &child);
1113
-
1114
- comrak_root_node.append(new_child);
1115
- }
1222
+ let comrak_root_node = rb_self.to_comrak_node(&arena);
1116
1223
 
1117
1224
  let mut output = String::new();
1118
1225
  match comrak::format_html_with_plugins(
1119
- &comrak_root_node,
1226
+ comrak_root_node,
1120
1227
  &comrak_options,
1121
1228
  &mut output,
1122
1229
  &comrak_plugins,
@@ -1177,42 +1284,18 @@ impl CommonmarkerNode {
1177
1284
  };
1178
1285
 
1179
1286
  match syntect_adapter {
1180
- Some(ref adapter) => comrak_plugins.render.codefence_syntax_highlighter = Some(adapter),
1287
+ Some(ref adapter) => {
1288
+ comrak_plugins.render.codefence_syntax_highlighter = Some(adapter.as_ref())
1289
+ }
1181
1290
  None => comrak_plugins.render.codefence_syntax_highlighter = None,
1182
1291
  }
1183
1292
 
1184
1293
  let arena: Arena<ComrakAstNode> = Arena::new();
1185
- fn iter_nodes<'a>(
1186
- arena: &'a Arena<comrak::arena_tree::Node<'a, RefCell<ComrakAst>>>,
1187
- node: &CommonmarkerNode,
1188
- ) -> &'a comrak::arena_tree::Node<'a, std::cell::RefCell<comrak::nodes::Ast>> {
1189
- let comrak_node: &'a mut ComrakAstNode = arena.alloc(ComrakNode::new(RefCell::new(
1190
- node.inner.borrow().data.clone(),
1191
- )));
1192
-
1193
- for c in node.inner.children() {
1194
- let child = CommonmarkerNode { inner: c };
1195
- let child_node = iter_nodes(arena, &child);
1196
- comrak_node.append(child_node);
1197
- }
1198
-
1199
- comrak_node
1200
- }
1201
-
1202
- let comrak_root_node: ComrakNode<RefCell<ComrakAst>> =
1203
- ComrakNode::new(RefCell::new(rb_self.inner.borrow().data.clone()));
1204
-
1205
- for c in rb_self.inner.children() {
1206
- let child = CommonmarkerNode { inner: c };
1207
-
1208
- let new_child = iter_nodes(&arena, &child);
1209
-
1210
- comrak_root_node.append(new_child);
1211
- }
1294
+ let comrak_root_node = rb_self.to_comrak_node(&arena);
1212
1295
 
1213
1296
  let mut output = String::new();
1214
1297
  match comrak::format_commonmark_with_plugins(
1215
- &comrak_root_node,
1298
+ comrak_root_node,
1216
1299
  &comrak_options,
1217
1300
  &mut output,
1218
1301
  &comrak_plugins,
@@ -1221,7 +1304,7 @@ impl CommonmarkerNode {
1221
1304
  Err(e) => {
1222
1305
  return Err(magnus::Error::new(
1223
1306
  ruby.exception_runtime_error(),
1224
- format!("cannot convert into html: {}", e),
1307
+ format!("cannot convert into commonmark: {}", e),
1225
1308
  ));
1226
1309
  }
1227
1310
  }
@@ -1292,6 +1375,14 @@ pub fn init(ruby: &Ruby, m_commonmarker: RModule) -> Result<(), magnus::Error> {
1292
1375
  method!(CommonmarkerNode::set_string_content, 1),
1293
1376
  )?;
1294
1377
 
1378
+ c_node.define_method("literal", method!(CommonmarkerNode::get_literal, 0))?;
1379
+ c_node.define_method("literal=", method!(CommonmarkerNode::set_literal, 1))?;
1380
+
1381
+ c_node.define_method(
1382
+ "node_supports?",
1383
+ method!(CommonmarkerNode::supports_method, 1),
1384
+ )?;
1385
+
1295
1386
  c_node.define_method("url", method!(CommonmarkerNode::get_url, 0))?;
1296
1387
  c_node.define_method("url=", method!(CommonmarkerNode::set_url, 1))?;
1297
1388
  c_node.define_method("title", method!(CommonmarkerNode::get_title, 0))?;
@@ -117,7 +117,6 @@ pub fn iterate_render_options(comrak_options: &mut comrak::options::Render, opti
117
117
  }
118
118
 
119
119
  const EXTENSION_STRIKETHROUGH: &str = "strikethrough";
120
- const EXTENSION_TAGFILTER: &str = "tagfilter";
121
120
  const EXTENSION_TABLE: &str = "table";
122
121
  const EXTENSION_AUTOLINK: &str = "autolink";
123
122
  const EXTENSION_TASKLIST: &str = "tasklist";
@@ -156,9 +155,6 @@ pub fn iterate_extension_options(
156
155
  Cow::Borrowed(EXTENSION_STRIKETHROUGH) => {
157
156
  comrak_options.strikethrough = TryConvert::try_convert(value)?;
158
157
  }
159
- Cow::Borrowed(EXTENSION_TAGFILTER) => {
160
- comrak_options.tagfilter = TryConvert::try_convert(value)?;
161
- }
162
158
  Cow::Borrowed(EXTENSION_TABLE) => {
163
159
  comrak_options.table = TryConvert::try_convert(value)?;
164
160
  }
@@ -1,4 +1,5 @@
1
1
  use std::path::PathBuf;
2
+ use std::sync::{Arc, OnceLock};
2
3
 
3
4
  use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
4
5
 
@@ -11,7 +12,7 @@ use crate::EMPTY_STR;
11
12
  pub fn construct_syntax_highlighter_from_plugin(
12
13
  ruby: &Ruby,
13
14
  rb_plugins: Option<RHash>,
14
- ) -> Result<Option<SyntectAdapter>, magnus::Error> {
15
+ ) -> Result<Option<Arc<SyntectAdapter>>, magnus::Error> {
15
16
  match rb_plugins {
16
17
  None => Ok(None),
17
18
  Some(rb_plugins) => {
@@ -34,8 +35,11 @@ pub fn construct_syntax_highlighter_from_plugin(
34
35
  Some(theme) => {
35
36
  if theme.is_empty() {
36
37
  // no theme? uss css classes
37
- adapter = SyntectAdapter::new(None);
38
- Ok(Some(adapter))
38
+ // Avoid reloading syntax sets and recompiling regexes for every CSS-mode render.
39
+ static CSS_ADAPTER: OnceLock<Arc<SyntectAdapter>> = OnceLock::new();
40
+ Ok(Some(Arc::clone(
41
+ CSS_ADAPTER.get_or_init(|| Arc::new(SyntectAdapter::new(None))),
42
+ )))
39
43
  } else {
40
44
  let path = match rb_plugins
41
45
  .get(ruby.to_symbol(super::SYNTAX_HIGHLIGHTER_PLUGIN))
@@ -80,7 +84,7 @@ pub fn construct_syntax_highlighter_from_plugin(
80
84
 
81
85
  adapter = builder.theme_set(ts).theme(&theme).build();
82
86
 
83
- Ok(Some(adapter))
87
+ Ok(Some(Arc::new(adapter)))
84
88
  } else {
85
89
  // no path? default theme lookup
86
90
  ThemeSet::load_defaults()
@@ -93,7 +97,7 @@ pub fn construct_syntax_highlighter_from_plugin(
93
97
  )
94
98
  })?;
95
99
  adapter = SyntectAdapter::new(Some(&theme));
96
- Ok(Some(adapter))
100
+ Ok(Some(Arc::new(adapter)))
97
101
  }
98
102
  }
99
103
  }