gitlab-glfm-markdown 0.0.33-arm64-darwin → 0.0.35-arm64-darwin

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: def60b50edf5747cfa78e8a4d3baf07b4e122206702ff0b3e36981a46a365fb0
4
- data.tar.gz: 7c8fa6c71b231c51330e7707a3daa17ed10544d55a3ce2a34ba6b6f6210774f7
3
+ metadata.gz: 528a1af01377e71c871ff26596d21ba821ff0c25d693bd475c0be9675805f01d
4
+ data.tar.gz: c879a3ce7d24e3ae37f07b87f255316b99b7d412a5f8468749853c48829e0bef
5
5
  SHA512:
6
- metadata.gz: 8d8884ffa3653002346d668bb7d1ce962a55346744594321abf459d2e73e2ff7102b97708cd40fc1a50e7213051e17f2c5f87b20f2f295298ee10f1a19efa188
7
- data.tar.gz: 63bd484230a28deb24ad991c852df2deab3ae4998528ac4ca2fa9b6cd74d65b7311ea95b4fee1ce09b22bca83aad20d52ec9338f1457906026faac8141d3b510
6
+ metadata.gz: 6a4aa3ce319503fe76d5c7ad69268eebed4320761e7661fab70503a5f6b3c870ce451fe3626b683c8818aca940127feca195f024bc1e4831072b5e839ee26262
7
+ data.tar.gz: b5c0d750df307f9704d0428f31ff3bfdcfebe44637ae6c0930c40bb489d7a52779b618da373ac957bbf8dbde40fff4ebc971668fc0f96aca2e3bc475b016c15f
data/Cargo.lock CHANGED
@@ -227,9 +227,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
227
227
 
228
228
  [[package]]
229
229
  name = "comrak"
230
- version = "0.40.0"
230
+ version = "0.43.0"
231
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
- checksum = "32c3278f396e5707769a68bc0943e9b8f84a172836b173827810918279621747"
232
+ checksum = "5ccfd7678fba9aff54a74a70352d952b345e568823ed9d5a92ebc8ad0adad8ea"
233
233
  dependencies = [
234
234
  "caseless",
235
235
  "emojis",
@@ -320,7 +320,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
320
320
 
321
321
  [[package]]
322
322
  name = "glfm_markdown"
323
- version = "0.0.33"
323
+ version = "0.0.35"
324
324
  dependencies = [
325
325
  "clap",
326
326
  "comrak",
@@ -328,6 +328,8 @@ dependencies = [
328
328
  "magnus",
329
329
  "rb-sys",
330
330
  "regex",
331
+ "serde",
332
+ "serde_magnus",
331
333
  ]
332
334
 
333
335
  [[package]]
@@ -707,6 +709,17 @@ dependencies = [
707
709
  "serde",
708
710
  ]
709
711
 
712
+ [[package]]
713
+ name = "serde_magnus"
714
+ version = "0.8.1"
715
+ source = "registry+https://github.com/rust-lang/crates.io-index"
716
+ checksum = "76c20da583b5e1016e9199ef5f3260f7a8d1b253307d232600f6b12737262dbd"
717
+ dependencies = [
718
+ "magnus",
719
+ "serde",
720
+ "tap",
721
+ ]
722
+
710
723
  [[package]]
711
724
  name = "shell-words"
712
725
  version = "1.1.0"
@@ -775,6 +788,12 @@ dependencies = [
775
788
  "yaml-rust",
776
789
  ]
777
790
 
791
+ [[package]]
792
+ name = "tap"
793
+ version = "1.0.1"
794
+ source = "registry+https://github.com/rust-lang/crates.io-index"
795
+ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
796
+
778
797
  [[package]]
779
798
  name = "thiserror"
780
799
  version = "1.0.69"
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Implements GLFM (as used by GitLab) using the Rust-based markdown parser [comrak](https://github.com/kivikakk/comrak)
7
7
  and providing a Ruby interface.\
8
- _Currently using `comrak 0.40.0`_.
8
+ _Currently using `comrak 0.43.0`_.
9
9
 
10
10
  This project is still in constant flux, so interfaces and functionality can change at any time.
11
11
 
@@ -27,6 +27,8 @@ Try on command line:
27
27
  rake compile
28
28
  bin/console
29
29
 
30
+ require 'glfm_markdown'
31
+
30
32
  GLFMMarkdown.to_html('# header', options: { sourcepos: true })
31
33
  ```
32
34
 
@@ -95,7 +97,7 @@ of Ruby for the project and the gem. Otherwise you can see unexplained errors wh
95
97
  calling into the gem.
96
98
 
97
99
  NOTE: This project generates a changelog automatically that gets attached to the release entry.
98
- The normal [GitLab changelog entry process](https://docs.gitlab.com/ee/development/changelog.html)
100
+ The normal [GitLab changelog entry process](https://docs.gitlab.com/development/changelog.html)
99
101
  should be followed.
100
102
 
101
103
  ### Releasing a new version
@@ -227,9 +227,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
227
227
 
228
228
  [[package]]
229
229
  name = "comrak"
230
- version = "0.40.0"
230
+ version = "0.43.0"
231
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
- checksum = "32c3278f396e5707769a68bc0943e9b8f84a172836b173827810918279621747"
232
+ checksum = "5ccfd7678fba9aff54a74a70352d952b345e568823ed9d5a92ebc8ad0adad8ea"
233
233
  dependencies = [
234
234
  "caseless",
235
235
  "emojis",
@@ -320,7 +320,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
320
320
 
321
321
  [[package]]
322
322
  name = "glfm_markdown"
323
- version = "0.0.33"
323
+ version = "0.0.35"
324
324
  dependencies = [
325
325
  "clap",
326
326
  "comrak",
@@ -328,6 +328,8 @@ dependencies = [
328
328
  "magnus",
329
329
  "rb-sys",
330
330
  "regex",
331
+ "serde",
332
+ "serde_magnus",
331
333
  ]
332
334
 
333
335
  [[package]]
@@ -707,6 +709,17 @@ dependencies = [
707
709
  "serde",
708
710
  ]
709
711
 
712
+ [[package]]
713
+ name = "serde_magnus"
714
+ version = "0.8.1"
715
+ source = "registry+https://github.com/rust-lang/crates.io-index"
716
+ checksum = "76c20da583b5e1016e9199ef5f3260f7a8d1b253307d232600f6b12737262dbd"
717
+ dependencies = [
718
+ "magnus",
719
+ "serde",
720
+ "tap",
721
+ ]
722
+
710
723
  [[package]]
711
724
  name = "shell-words"
712
725
  version = "1.1.0"
@@ -775,6 +788,12 @@ dependencies = [
775
788
  "yaml-rust",
776
789
  ]
777
790
 
791
+ [[package]]
792
+ name = "tap"
793
+ version = "1.0.1"
794
+ source = "registry+https://github.com/rust-lang/crates.io-index"
795
+ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
796
+
778
797
  [[package]]
779
798
  name = "thiserror"
780
799
  version = "1.0.69"
@@ -1,8 +1,8 @@
1
1
  [package]
2
2
  name = "glfm_markdown"
3
- version = "0.0.33"
3
+ version = "0.0.35"
4
4
  edition = "2021"
5
- authors = ["digitalmoksha <bwalker@gitlab.com>"]
5
+ authors = ["digitalmoksha <bwalker@gitlab.com>", "Asherah Connor <aconnor@gitlab.com>"]
6
6
  description = "GitLab Flavored Markdown parser and formatter. 100% CommonMark-compatible. Experimental."
7
7
  publish = false
8
8
 
@@ -15,11 +15,13 @@ required-features = ["cli"]
15
15
 
16
16
  [dependencies]
17
17
  clap = { version = "=4.4.18", optional = true, features = ["derive", "string"] }
18
- comrak = { version = "0.40.0", default-features = false, features = ["shortcodes"] }
18
+ comrak = { version = "0.43.0", default-features = false, features = ["shortcodes"] }
19
19
  magnus = "0.6.2"
20
20
  rb-sys = { version = "0.9.86", default-features = false, features = ["stable-api-compiled-fallback"] }
21
21
  regex = "1.11.1"
22
22
  lazy_static = "1.5.0"
23
+ serde_magnus = "0.8.1"
24
+ serde = { version = "1.0.219", features = ["serde_derive"] }
23
25
 
24
26
  [features]
25
27
  cli = ["clap", "comrak/syntect"]
@@ -0,0 +1,408 @@
1
+ use std::fmt::{self, Write};
2
+
3
+ use comrak::html::{collect_text, format_node_default, render_sourcepos, ChildRendering, Context};
4
+ use comrak::nodes::{AstNode, ListType, NodeValue};
5
+ use comrak::{create_formatter, html};
6
+ use lazy_static::lazy_static;
7
+ use regex::Regex;
8
+
9
+ // TODO: Use std::sync:LazyLock once we're on 1.80+.
10
+ // https://doc.rust-lang.org/std/sync/struct.LazyLock.html
11
+ lazy_static! {
12
+ static ref PLACEHOLDER_REGEX: Regex = Regex::new(r"%(\{|%7B)(\w{1,30})(}|%7D)").unwrap();
13
+ }
14
+
15
+ pub struct RenderUserData {
16
+ pub default_html: bool,
17
+ pub inapplicable_tasks: bool,
18
+ pub placeholder_detection: bool,
19
+ pub only_escape_chars: Option<Vec<char>>,
20
+ pub debug: bool,
21
+ }
22
+
23
+ // The important thing to remember is that this overrides the default behavior of the
24
+ // specified nodes. If we do override a node, then it's our responsibility to ensure that
25
+ // any changes in the `comrak` code for those nodes is backported to here, such as when
26
+ // `figcaption` support was added.
27
+ // One idea to limit that would be having the ability to specify attributes that would
28
+ // be inserted when a node is rendered. That would allow us to (in many cases) just
29
+ // inject the changes we need. Such a feature would need to be added to `comrak`.
30
+ create_formatter!(CustomFormatter<RenderUserData>, {
31
+ NodeValue::Text(_) => |context, node, entering| {
32
+ return render_text(context, node, entering);
33
+ },
34
+ NodeValue::Link(_) => |context, node, entering| {
35
+ return render_link(context, node, entering);
36
+ },
37
+ NodeValue::Image(_) => |context, node, entering| {
38
+ return render_image(context, node, entering);
39
+ },
40
+ NodeValue::List(_) => |context, node, entering| {
41
+ return render_list(context, node, entering);
42
+ },
43
+ NodeValue::TaskItem(_) => |context, node, entering| {
44
+ return render_task_item(context, node, entering);
45
+ },
46
+ NodeValue::Heading(_) => |context, node, entering| {
47
+ return render_heading(context, node, entering);
48
+ },
49
+ NodeValue::Escaped => |context, node, entering| {
50
+ return render_escaped(context, node, entering);
51
+ },
52
+ });
53
+
54
+ fn render_image<'a>(
55
+ context: &mut Context<RenderUserData>,
56
+ node: &'a AstNode<'a>,
57
+ entering: bool,
58
+ ) -> Result<ChildRendering, fmt::Error> {
59
+ let NodeValue::Image(ref nl) = node.data.borrow().value else {
60
+ unreachable!()
61
+ };
62
+
63
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
64
+ return html::format_node_default(context, node, entering);
65
+ }
66
+
67
+ if entering {
68
+ if context.options.render.figure_with_caption {
69
+ context.write_str("<figure>")?;
70
+ }
71
+ context.write_str("<img")?;
72
+ html::render_sourcepos(context, node)?;
73
+ context.write_str(" src=\"")?;
74
+ if context.options.render.unsafe_ || !html::dangerous_url(&nl.url) {
75
+ if let Some(rewriter) = &context.options.extension.image_url_rewriter {
76
+ context.escape_href(&rewriter.to_html(&nl.url))?;
77
+ } else {
78
+ context.escape_href(&nl.url)?;
79
+ }
80
+ }
81
+
82
+ context.write_str("\"")?;
83
+
84
+ if PLACEHOLDER_REGEX.is_match(&nl.url) {
85
+ context.write_str(" data-placeholder")?;
86
+ }
87
+
88
+ context.write_str(" alt=\"")?;
89
+
90
+ return Ok(ChildRendering::Plain);
91
+ } else {
92
+ if !nl.title.is_empty() {
93
+ context.write_str("\" title=\"")?;
94
+ context.escape(&nl.title)?;
95
+ }
96
+ context.write_str("\" />")?;
97
+ if context.options.render.figure_with_caption {
98
+ if !nl.title.is_empty() {
99
+ context.write_str("<figcaption>")?;
100
+ context.escape(&nl.title)?;
101
+ context.write_str("</figcaption>")?;
102
+ }
103
+ context.write_str("</figure>")?;
104
+ };
105
+ }
106
+
107
+ Ok(ChildRendering::HTML)
108
+ }
109
+
110
+ fn render_link<'a>(
111
+ context: &mut Context<RenderUserData>,
112
+ node: &'a AstNode<'a>,
113
+ entering: bool,
114
+ ) -> Result<ChildRendering, fmt::Error> {
115
+ let NodeValue::Link(ref nl) = node.data.borrow().value else {
116
+ unreachable!()
117
+ };
118
+
119
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
120
+ return html::format_node_default(context, node, entering);
121
+ }
122
+
123
+ let parent_node = node.parent();
124
+
125
+ if !context.options.parse.relaxed_autolinks
126
+ || (parent_node.is_none()
127
+ || !matches!(
128
+ parent_node.unwrap().data.borrow().value,
129
+ NodeValue::Link(..)
130
+ ))
131
+ {
132
+ if entering {
133
+ context.write_str("<a")?;
134
+ html::render_sourcepos(context, node)?;
135
+ context.write_str(" href=\"")?;
136
+ if context.options.render.unsafe_ || !html::dangerous_url(&nl.url) {
137
+ if let Some(rewriter) = &context.options.extension.link_url_rewriter {
138
+ context.escape_href(&rewriter.to_html(&nl.url))?;
139
+ } else {
140
+ context.escape_href(&nl.url)?;
141
+ }
142
+ }
143
+ context.write_str("\"")?;
144
+
145
+ if !nl.title.is_empty() {
146
+ context.write_str(" title=\"")?;
147
+ context.escape(&nl.title)?;
148
+ }
149
+
150
+ if PLACEHOLDER_REGEX.is_match(&nl.url) {
151
+ context.write_str(" data-placeholder")?;
152
+ }
153
+
154
+ context.write_str(">")?;
155
+ } else {
156
+ context.write_str("</a>")?;
157
+ }
158
+ }
159
+
160
+ Ok(ChildRendering::HTML)
161
+ }
162
+
163
+ // Overridden to use class `task-list` instead of `contains-task-list`
164
+ // to align with GitLab class usage
165
+ fn render_list<'a>(
166
+ context: &mut Context<RenderUserData>,
167
+ node: &'a AstNode<'a>,
168
+ entering: bool,
169
+ ) -> Result<ChildRendering, fmt::Error> {
170
+ if !entering || !context.options.render.tasklist_classes {
171
+ return html::format_node_default(context, node, entering);
172
+ }
173
+
174
+ let NodeValue::List(ref nl) = node.data.borrow().value else {
175
+ unreachable!()
176
+ };
177
+
178
+ context.cr()?;
179
+ match nl.list_type {
180
+ ListType::Bullet => {
181
+ context.write_str("<ul")?;
182
+ if nl.is_task_list {
183
+ context.write_str(" class=\"task-list\"")?;
184
+ }
185
+ html::render_sourcepos(context, node)?;
186
+ context.write_str(">\n")?;
187
+ }
188
+ ListType::Ordered => {
189
+ context.write_str("<ol")?;
190
+ if nl.is_task_list {
191
+ context.write_str(" class=\"task-list\"")?;
192
+ }
193
+ html::render_sourcepos(context, node)?;
194
+ if nl.start == 1 {
195
+ context.write_str(">\n")?;
196
+ } else {
197
+ writeln!(context, " start=\"{}\">", nl.start)?;
198
+ }
199
+ }
200
+ }
201
+
202
+ Ok(ChildRendering::HTML)
203
+ }
204
+
205
+ // Overridden to detect inapplicable task list items
206
+ fn render_task_item<'a>(
207
+ context: &mut Context<RenderUserData>,
208
+ node: &'a AstNode<'a>,
209
+ entering: bool,
210
+ ) -> Result<ChildRendering, fmt::Error> {
211
+ if !context.user.inapplicable_tasks {
212
+ return html::format_node_default(context, node, entering);
213
+ }
214
+
215
+ let NodeValue::TaskItem(symbol) = node.data.borrow().value else {
216
+ unreachable!()
217
+ };
218
+
219
+ if symbol.is_none() || matches!(symbol, Some('x' | 'X')) {
220
+ return html::format_node_default(context, node, entering);
221
+ }
222
+
223
+ if entering {
224
+ // Handle an inapplicable task symbol.
225
+ if matches!(symbol, Some('~')) {
226
+ context.cr()?;
227
+ context.write_str("<li")?;
228
+ context.write_str(" class=\"inapplicable")?;
229
+
230
+ if context.options.render.tasklist_classes {
231
+ context.write_str(" task-list-item")?;
232
+ }
233
+ context.write_str("\"")?;
234
+
235
+ html::render_sourcepos(context, node)?;
236
+ context.write_str(">")?;
237
+ context.write_str("<input type=\"checkbox\"")?;
238
+
239
+ if context.options.render.tasklist_classes {
240
+ context.write_str(" class=\"task-list-item-checkbox\"")?;
241
+ }
242
+
243
+ context.write_str(" data-inapplicable disabled=\"\"> ")?;
244
+ } else {
245
+ // Don't allow unsupported symbols to render a checkbox
246
+ context.cr()?;
247
+ context.write_str("<li")?;
248
+
249
+ if context.options.render.tasklist_classes {
250
+ context.write_str(" class=\"task-list-item\"")?;
251
+ }
252
+
253
+ html::render_sourcepos(context, node)?;
254
+ context.write_str(">")?;
255
+ context.write_str("[")?;
256
+ context.escape(&symbol.unwrap().to_string())?;
257
+ context.write_str("] ")?;
258
+ }
259
+ } else {
260
+ context.write_str("</li>\n")?;
261
+ }
262
+
263
+ Ok(ChildRendering::HTML)
264
+ }
265
+
266
+ fn render_text<'a>(
267
+ context: &mut Context<RenderUserData>,
268
+ node: &'a AstNode<'a>,
269
+ entering: bool,
270
+ ) -> Result<ChildRendering, fmt::Error> {
271
+ let NodeValue::Text(ref literal) = node.data.borrow().value else {
272
+ unreachable!()
273
+ };
274
+
275
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
276
+ return html::format_node_default(context, node, entering);
277
+ }
278
+
279
+ // Don't currently support placeholders in the text inside links or images.
280
+ // If the text has an underscore in it, then the parser will not combine
281
+ // the multiple text nodes in `comrak`'s `postprocess_text_nodes`, breaking up
282
+ // the placeholder into multiple text nodes.
283
+ // For example, `[%{a_b}](link)`.
284
+ let parent = node.parent().unwrap();
285
+ if matches!(
286
+ parent.data.borrow().value,
287
+ NodeValue::Link(_) | NodeValue::Image(_)
288
+ ) {
289
+ return html::format_node_default(context, node, entering);
290
+ }
291
+
292
+ if entering {
293
+ let mut cursor: usize = 0;
294
+
295
+ for mat in PLACEHOLDER_REGEX.find_iter(literal) {
296
+ if mat.start() > cursor {
297
+ context.escape(&literal[cursor..mat.start()])?;
298
+ }
299
+
300
+ context.write_str("<span data-placeholder>")?;
301
+ context.escape(&literal[mat.start()..mat.end()])?;
302
+ context.write_str("</span>")?;
303
+
304
+ cursor = mat.end();
305
+ }
306
+
307
+ if cursor < literal.len() {
308
+ context.escape(&literal[cursor..literal.len()])?;
309
+ }
310
+ }
311
+
312
+ Ok(ChildRendering::HTML)
313
+ }
314
+
315
+ fn render_heading<'a>(
316
+ context: &mut Context<RenderUserData>,
317
+ node: &'a AstNode<'a>,
318
+ entering: bool,
319
+ ) -> Result<ChildRendering, fmt::Error> {
320
+ let NodeValue::Heading(ref nh) = node.data.borrow().value else {
321
+ unreachable!()
322
+ };
323
+
324
+ match context.plugins.render.heading_adapter {
325
+ None => {
326
+ // Overrides the default handling in order to render the heading text
327
+ // inside the anchor tag to better support screen readers
328
+ if entering {
329
+ context.cr()?;
330
+ write!(context, "<h{}", nh.level)?;
331
+ render_sourcepos(context, node)?;
332
+ context.write_str(">")?;
333
+
334
+ if let Some(ref prefix) = context.options.extension.header_ids {
335
+ let text_content = collect_text(node);
336
+ let id = context.anchorizer.anchorize(&text_content);
337
+ write!(
338
+ context,
339
+ r##"<a href="#{id}" class="anchor" id="{prefix}{id}">"##
340
+ )?;
341
+ }
342
+ } else {
343
+ if context.options.extension.header_ids.is_some() {
344
+ write!(context, "</a>")?;
345
+ }
346
+ writeln!(context, "</h{}>", nh.level)?;
347
+ }
348
+ Ok(ChildRendering::HTML)
349
+ }
350
+ Some(_adapter) => format_node_default(context, node, entering),
351
+ }
352
+ }
353
+
354
+ fn render_escaped<'a>(
355
+ context: &mut Context<RenderUserData>,
356
+ node: &'a AstNode<'a>,
357
+ entering: bool,
358
+ ) -> Result<ChildRendering, fmt::Error> {
359
+ if !context.options.render.escaped_char_spans {
360
+ return Ok(ChildRendering::HTML);
361
+ }
362
+
363
+ if context.user.only_escape_chars.is_none()
364
+ || with_node_text_content(node, false, |content| {
365
+ if content.len() != 1 {
366
+ return false;
367
+ }
368
+ let c = content.chars().next().unwrap();
369
+ context
370
+ .user
371
+ .only_escape_chars
372
+ .as_ref()
373
+ .unwrap()
374
+ .contains(&c)
375
+ })
376
+ {
377
+ if entering {
378
+ context.write_str("<span data-escaped-char")?;
379
+ render_sourcepos(context, node)?;
380
+ context.write_str(">")?;
381
+ } else {
382
+ context.write_str("</span>")?;
383
+ }
384
+ }
385
+
386
+ Ok(ChildRendering::HTML)
387
+ }
388
+
389
+ /// If the given node has a single text child, apply a function to the text content
390
+ /// of that node. Otherwise, return the given default value.
391
+ fn with_node_text_content<'a, U, F>(node: &'a AstNode<'a>, default: U, f: F) -> U
392
+ where
393
+ F: FnOnce(&str) -> U,
394
+ {
395
+ let Some(child) = node.first_child() else {
396
+ return default;
397
+ };
398
+ let Some(last_child) = node.last_child() else {
399
+ return default;
400
+ };
401
+ if !child.same_node(last_child) {
402
+ return default;
403
+ }
404
+ let NodeValue::Text(ref text) = child.data.borrow().value else {
405
+ return default;
406
+ };
407
+ f(text)
408
+ }
@@ -1,16 +1,10 @@
1
- use comrak::html::{ChildRendering, Context};
2
- use comrak::nodes::{AstNode, ListType, NodeValue};
3
- use comrak::{create_formatter, html, parse_document, Arena, Plugins};
4
- use lazy_static::lazy_static;
5
- use regex::Regex;
6
- use std::io;
7
- use std::io::{BufWriter, Write};
1
+ use comrak::{parse_document, Arena, Plugins};
2
+ use serde::Deserialize;
8
3
 
9
- lazy_static! {
10
- static ref PLACEHOLDER_REGEX: Regex = Regex::new(r"%(\{|%7B)(\w{1,30})(}|%7D)").unwrap();
11
- }
4
+ use crate::formatter::{CustomFormatter, RenderUserData};
12
5
 
13
- #[derive(Debug, Clone)]
6
+ #[derive(Debug, Default, Deserialize)]
7
+ #[serde(default)]
14
8
  pub struct RenderOptions {
15
9
  pub alerts: bool,
16
10
  pub autolink: bool,
@@ -47,7 +41,7 @@ pub struct RenderOptions {
47
41
  pub tasklist: bool,
48
42
  pub tasklist_classes: bool,
49
43
  pub underline: bool,
50
- pub unsafe_: bool,
44
+ pub r#unsafe: bool,
51
45
  pub wikilinks_title_after_pipe: bool,
52
46
  pub wikilinks_title_before_pipe: bool,
53
47
 
@@ -63,13 +57,10 @@ pub struct RenderOptions {
63
57
  /// have the format `%{PLACEHOLDER}`
64
58
  pub placeholder_detection: bool,
65
59
 
66
- pub debug: bool,
67
- }
60
+ /// When the 'escaped_char_spans' render option is enabled, only emit `<span
61
+ /// data-escaped-char>` around the given characters.
62
+ pub only_escape_chars: Option<Vec<char>>,
68
63
 
69
- pub struct RenderUserData {
70
- pub default_html: bool,
71
- pub inapplicable_tasks: bool,
72
- pub placeholder_detection: bool,
73
64
  pub debug: bool,
74
65
  }
75
66
 
@@ -79,6 +70,7 @@ impl From<&RenderOptions> for RenderUserData {
79
70
  default_html: options.default_html,
80
71
  inapplicable_tasks: options.inapplicable_tasks,
81
72
  placeholder_detection: options.placeholder_detection,
73
+ only_escape_chars: options.only_escape_chars.clone(),
82
74
  debug: options.debug,
83
75
  }
84
76
  }
@@ -123,7 +115,7 @@ impl From<&RenderOptions> for comrak::Options<'_> {
123
115
  comrak_options.render.tasklist_classes = options.tasklist_classes;
124
116
  // comrak_options.render.syntax_highlighting = options.syntax_highlighting;
125
117
 
126
- comrak_options.render.unsafe_ = options.unsafe_;
118
+ comrak_options.render.unsafe_ = options.r#unsafe;
127
119
 
128
120
  // comrak_options.parse.default_info_string = options.default_info_string;
129
121
  comrak_options.parse.relaxed_autolinks = options.relaxed_autolinks;
@@ -134,310 +126,23 @@ impl From<&RenderOptions> for comrak::Options<'_> {
134
126
  }
135
127
  }
136
128
 
137
- pub fn render(text: String, options: RenderOptions) -> String {
129
+ pub fn render(text: &str, options: &RenderOptions) -> String {
138
130
  render_with_plugins(text, options, &comrak::Plugins::default())
139
131
  }
140
132
 
141
- fn render_with_plugins(text: String, render_options: RenderOptions, plugins: &Plugins) -> String {
142
- let user_data = RenderUserData::from(&render_options);
143
- let options = comrak::Options::from(&render_options);
133
+ fn render_with_plugins(text: &str, render_options: &RenderOptions, plugins: &Plugins) -> String {
134
+ let user_data = RenderUserData::from(render_options);
135
+ let options = comrak::Options::from(render_options);
144
136
 
145
137
  if user_data.default_html {
146
- return comrak::markdown_to_html_with_plugins(&text, &options, plugins);
138
+ return comrak::markdown_to_html_with_plugins(text, &options, plugins);
147
139
  }
148
140
 
149
141
  let arena = Arena::new();
150
- let root = parse_document(&arena, &text, &options);
151
- let mut bw = BufWriter::new(Vec::new());
142
+ let root = parse_document(&arena, text, &options);
152
143
 
153
- CustomFormatter::format_document_with_plugins(root, &options, &mut bw, plugins, user_data)
144
+ let mut out = String::new();
145
+ CustomFormatter::format_document_with_plugins(root, &options, &mut out, plugins, user_data)
154
146
  .unwrap();
155
- String::from_utf8(bw.into_inner().unwrap()).unwrap()
156
- }
157
-
158
- // The important thing to remember is that this overrides the default behavior of the
159
- // specified nodes. If we do override a node, then it's our responsibility to ensure that
160
- // any changes in the `comrak` code for those nodes is backported to here, such as when
161
- // `figcaption` support was added.
162
- // One idea to limit that would be having the ability to specify attributes that would
163
- // be inserted when a node is rendered. That would allow us to (in many cases) just
164
- // inject the changes we need. Such a feature would need to be added to `comrak`.
165
- create_formatter!(CustomFormatter<RenderUserData>, {
166
- NodeValue::Text(_) => |context, node, entering| {
167
- return render_text(context, node, entering);
168
- },
169
- NodeValue::Link(_) => |context, node, entering| {
170
- return render_link(context, node, entering);
171
- },
172
- NodeValue::Image(_) => |context, node, entering| {
173
- return render_image(context, node, entering);
174
- },
175
- NodeValue::List(_) => |context, node, entering| {
176
- return render_list(context, node, entering);
177
- },
178
- NodeValue::TaskItem(_) => |context, node, entering| {
179
- return render_task_item(context, node, entering);
180
- }
181
- });
182
-
183
- fn render_image<'a>(
184
- context: &mut Context<RenderUserData>,
185
- node: &'a AstNode<'a>,
186
- entering: bool,
187
- ) -> io::Result<ChildRendering> {
188
- let NodeValue::Image(ref nl) = node.data.borrow().value else {
189
- panic!("Attempt to render invalid node as image")
190
- };
191
-
192
- if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
193
- return html::format_node_default(context, node, entering);
194
- }
195
-
196
- if entering {
197
- if context.options.render.figure_with_caption {
198
- context.write_all(b"<figure>")?;
199
- }
200
- context.write_all(b"<img")?;
201
- html::render_sourcepos(context, node)?;
202
- context.write_all(b" src=\"")?;
203
- let url = nl.url.as_bytes();
204
- if context.options.render.unsafe_ || !html::dangerous_url(url) {
205
- if let Some(rewriter) = &context.options.extension.image_url_rewriter {
206
- context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
207
- } else {
208
- context.escape_href(url)?;
209
- }
210
- }
211
-
212
- context.write_all(b"\"")?;
213
-
214
- if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
215
- context.write_all(b" data-placeholder")?;
216
- }
217
-
218
- context.write_all(b" alt=\"")?;
219
-
220
- return Ok(ChildRendering::Plain);
221
- } else {
222
- if !nl.title.is_empty() {
223
- context.write_all(b"\" title=\"")?;
224
- context.escape(nl.title.as_bytes())?;
225
- }
226
- context.write_all(b"\" />")?;
227
- if context.options.render.figure_with_caption {
228
- if !nl.title.is_empty() {
229
- context.write_all(b"<figcaption>")?;
230
- context.escape(nl.title.as_bytes())?;
231
- context.write_all(b"</figcaption>")?;
232
- }
233
- context.write_all(b"</figure>")?;
234
- };
235
- }
236
-
237
- Ok(ChildRendering::HTML)
238
- }
239
-
240
- fn render_link<'a>(
241
- context: &mut Context<RenderUserData>,
242
- node: &'a AstNode<'a>,
243
- entering: bool,
244
- ) -> io::Result<ChildRendering> {
245
- let NodeValue::Link(ref nl) = node.data.borrow().value else {
246
- panic!("Attempt to render invalid node as link")
247
- };
248
-
249
- if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
250
- return html::format_node_default(context, node, entering);
251
- }
252
-
253
- let parent_node = node.parent();
254
-
255
- if !context.options.parse.relaxed_autolinks
256
- || (parent_node.is_none()
257
- || !matches!(
258
- parent_node.unwrap().data.borrow().value,
259
- NodeValue::Link(..)
260
- ))
261
- {
262
- if entering {
263
- context.write_all(b"<a")?;
264
- html::render_sourcepos(context, node)?;
265
- context.write_all(b" href=\"")?;
266
- let url = nl.url.as_bytes();
267
- if context.options.render.unsafe_ || !html::dangerous_url(url) {
268
- if let Some(rewriter) = &context.options.extension.link_url_rewriter {
269
- context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
270
- } else {
271
- context.escape_href(url)?;
272
- }
273
- }
274
- context.write_all(b"\"")?;
275
-
276
- if !nl.title.is_empty() {
277
- context.write_all(b" title=\"")?;
278
- context.escape(nl.title.as_bytes())?;
279
- }
280
-
281
- if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
282
- context.write_all(b" data-placeholder")?;
283
- }
284
-
285
- context.write_all(b">")?;
286
- } else {
287
- context.write_all(b"</a>")?;
288
- }
289
- }
290
-
291
- Ok(ChildRendering::HTML)
292
- }
293
-
294
- // Overridden to use class `task-list` instead of `contains-task-list`
295
- // to align with GitLab class usage
296
- fn render_list<'a>(
297
- context: &mut Context<RenderUserData>,
298
- node: &'a AstNode<'a>,
299
- entering: bool,
300
- ) -> io::Result<ChildRendering> {
301
- if !entering || !context.options.render.tasklist_classes {
302
- return html::format_node_default(context, node, entering);
303
- }
304
-
305
- let NodeValue::List(ref nl) = node.data.borrow().value else {
306
- panic!("Attempt to render invalid node as list")
307
- };
308
-
309
- context.cr()?;
310
- match nl.list_type {
311
- ListType::Bullet => {
312
- context.write_all(b"<ul")?;
313
- if nl.is_task_list {
314
- context.write_all(b" class=\"task-list\"")?;
315
- }
316
- html::render_sourcepos(context, node)?;
317
- context.write_all(b">\n")?;
318
- }
319
- ListType::Ordered => {
320
- context.write_all(b"<ol")?;
321
- if nl.is_task_list {
322
- context.write_all(b" class=\"task-list\"")?;
323
- }
324
- html::render_sourcepos(context, node)?;
325
- if nl.start == 1 {
326
- context.write_all(b">\n")?;
327
- } else {
328
- writeln!(context, " start=\"{}\">", nl.start)?;
329
- }
330
- }
331
- }
332
-
333
- Ok(ChildRendering::HTML)
334
- }
335
-
336
- // Overridden to detect inapplicable task list items
337
- fn render_task_item<'a>(
338
- context: &mut Context<RenderUserData>,
339
- node: &'a AstNode<'a>,
340
- entering: bool,
341
- ) -> io::Result<ChildRendering> {
342
- if !context.user.inapplicable_tasks {
343
- return html::format_node_default(context, node, entering);
344
- }
345
-
346
- let NodeValue::TaskItem(symbol) = node.data.borrow().value else {
347
- panic!("Attempt to render invalid node as task item")
348
- };
349
-
350
- if symbol.is_none() || matches!(symbol, Some('x' | 'X')) {
351
- return html::format_node_default(context, node, entering);
352
- }
353
-
354
- if entering {
355
- // Handle an inapplicable task symbol.
356
- if matches!(symbol, Some('~')) {
357
- context.cr()?;
358
- context.write_all(b"<li")?;
359
- context.write_all(b" class=\"inapplicable")?;
360
-
361
- if context.options.render.tasklist_classes {
362
- context.write_all(b" task-list-item")?;
363
- }
364
- context.write_all(b"\"")?;
365
-
366
- html::render_sourcepos(context, node)?;
367
- context.write_all(b">")?;
368
- context.write_all(b"<input type=\"checkbox\"")?;
369
-
370
- if context.options.render.tasklist_classes {
371
- context.write_all(b" class=\"task-list-item-checkbox\"")?;
372
- }
373
-
374
- context.write_all(b" data-inapplicable disabled=\"\"> ")?;
375
- } else {
376
- // Don't allow unsupported symbols to render a checkbox
377
- context.cr()?;
378
- context.write_all(b"<li")?;
379
-
380
- if context.options.render.tasklist_classes {
381
- context.write_all(b" class=\"task-list-item\"")?;
382
- }
383
-
384
- html::render_sourcepos(context, node)?;
385
- context.write_all(b">")?;
386
- context.write_all(b"[")?;
387
- context.escape(symbol.unwrap().to_string().as_bytes())?;
388
- context.write_all(b"] ")?;
389
- }
390
- } else {
391
- context.write_all(b"</li>\n")?;
392
- }
393
-
394
- Ok(ChildRendering::HTML)
395
- }
396
- fn render_text<'a>(
397
- context: &mut Context<RenderUserData>,
398
- node: &'a AstNode<'a>,
399
- entering: bool,
400
- ) -> io::Result<ChildRendering> {
401
- let NodeValue::Text(ref literal) = node.data.borrow().value else {
402
- panic!("Attempt to render invalid node as text")
403
- };
404
-
405
- if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
406
- return html::format_node_default(context, node, entering);
407
- }
408
-
409
- // Don't currently support placeholders in the text inside links or images.
410
- // If the text has an underscore in it, then the parser will not combine
411
- // the multiple text nodes in `comrak`'s `postprocess_text_nodes`, breaking up
412
- // the placeholder into multiple text nodes.
413
- // For example, `[%{a_b}](link)`.
414
- let parent = node.parent().unwrap();
415
- if matches!(
416
- parent.data.borrow().value,
417
- NodeValue::Link(_) | NodeValue::Image(_)
418
- ) {
419
- return html::format_node_default(context, node, entering);
420
- }
421
-
422
- if entering {
423
- let mut cursor: usize = 0;
424
-
425
- for mat in PLACEHOLDER_REGEX.find_iter(literal) {
426
- if mat.start() > cursor {
427
- context.escape(literal[cursor..mat.start()].as_bytes())?;
428
- }
429
-
430
- context.write_all(b"<span data-placeholder>")?;
431
- context.escape(literal[mat.start()..mat.end()].as_bytes())?;
432
- context.write_all(b"</span>")?;
433
-
434
- cursor = mat.end();
435
- }
436
-
437
- if cursor < literal.len() {
438
- context.escape(literal[cursor..literal.len()].as_bytes())?;
439
- }
440
- }
441
-
442
- Ok(ChildRendering::HTML)
147
+ out
443
148
  }
@@ -1,66 +1,37 @@
1
- use magnus::{define_module, function, prelude::*, Error, RHash, Symbol};
1
+ use comrak::{escape_commonmark_inline, escape_commonmark_link_destination};
2
+ use magnus::{define_module, function, prelude::*, Error, RHash, RString};
3
+ use serde_magnus::deserialize;
2
4
 
5
+ mod formatter;
3
6
  mod glfm;
4
7
  use glfm::{render, RenderOptions};
5
8
 
6
- /// Lookup symbol in provided `RHash`. Returns `false` if the key is not present
7
- /// or value cannot be converted to a boolean.
8
- fn get_bool_opt(arg: &str, options: RHash) -> bool {
9
- options.lookup(Symbol::new(arg)).unwrap_or_default()
10
- }
9
+ pub fn render_to_html_rs(text: RString, options: RHash) -> Result<String, Error> {
10
+ let render_options: RenderOptions = deserialize(options)?;
11
11
 
12
- fn get_string_opt(arg: &str, options: RHash) -> Option<String> {
13
- options.lookup(Symbol::new(arg)).ok()
12
+ // SAFETY: `RString::as_str` returns a reference directly to Ruby memory.
13
+ // We do not hold onto or save the `&str`, or otherwise permit Ruby to GC or
14
+ // modify it while in `glfm::render`.
15
+ // https://docs.rs/magnus/latest/magnus/r_string/struct.RString.html#method.as_str
16
+ Ok(render(unsafe { text.as_str()? }, &render_options))
14
17
  }
15
18
 
16
- pub fn render_to_html_rs(text: String, options: RHash) -> String {
17
- let render_options = RenderOptions {
18
- alerts: get_bool_opt("alerts", options),
19
- autolink: get_bool_opt("autolink", options),
20
- // default_info_string: get_string_opt("default_info_string", options),
21
- description_lists: get_bool_opt("description_lists", options),
22
- escape: get_bool_opt("escape", options),
23
- escaped_char_spans: get_bool_opt("escaped_char_spans", options),
24
- figure_with_caption: get_bool_opt("figure_with_caption", options),
25
- footnotes: get_bool_opt("footnotes", options),
26
- // front_matter_delimiter: get_string_opt("front_matter_delimiter", options),
27
- full_info_string: get_bool_opt("full_info_string", options),
28
- gemojis: get_bool_opt("gemojis", options),
29
- gfm_quirks: get_bool_opt("gfm_quirks", options),
30
- github_pre_lang: get_bool_opt("github_pre_lang", options),
31
- greentext: get_bool_opt("greentext", options),
32
- hardbreaks: get_bool_opt("hardbreaks", options),
33
- header_ids: get_string_opt("header_ids", options),
34
- ignore_empty_links: get_bool_opt("ignore_empty_links", options),
35
- ignore_setext: get_bool_opt("ignore_setext", options),
36
- inapplicable_tasks: get_bool_opt("inapplicable_tasks", options),
37
- math_code: get_bool_opt("math_code", options),
38
- math_dollars: get_bool_opt("math_dollars", options),
39
- multiline_block_quotes: get_bool_opt("multiline_block_quotes", options),
40
- relaxed_autolinks: get_bool_opt("relaxed_autolinks", options),
41
- relaxed_tasklist_character: get_bool_opt("relaxed_tasklist_character", options),
42
- sourcepos: get_bool_opt("sourcepos", options),
43
- smart: get_bool_opt("smart", options),
44
- spoiler: get_bool_opt("spoiler", options),
45
- strikethrough: get_bool_opt("strikethrough", options),
46
- subscript: get_bool_opt("subscript", options),
47
- superscript: get_bool_opt("superscript", options),
48
- // syntax_highlighting: get_string_opt("syntax_highlighting", options),
49
- table: get_bool_opt("table", options),
50
- tagfilter: get_bool_opt("tagfilter", options),
51
- tasklist: get_bool_opt("tasklist", options),
52
- tasklist_classes: get_bool_opt("tasklist_classes", options),
53
- underline: get_bool_opt("underline", options),
54
- unsafe_: get_bool_opt("unsafe", options),
55
- wikilinks_title_after_pipe: get_bool_opt("wikilinks_title_after_pipe", options),
56
- wikilinks_title_before_pipe: get_bool_opt("wikilinks_title_before_pipe", options),
57
-
58
- default_html: get_bool_opt("default_html", options),
59
- placeholder_detection: get_bool_opt("placeholder_detection", options),
60
- debug: get_bool_opt("debug", options),
61
- };
19
+ pub fn escape_commonmark_inline_rs(text: RString) -> Result<String, Error> {
20
+ // SAFETY: `RString::as_str` returns a reference directly to Ruby memory.
21
+ // We do not hold onto or save the `&str`, or otherwise permit Ruby to GC or
22
+ // modify it while in `glfm::escape_commonmark_inline`.
23
+ // https://docs.rs/magnus/latest/magnus/r_string/struct.RString.html#method.as_str
24
+ Ok(escape_commonmark_inline(unsafe { text.as_str()? }))
25
+ }
62
26
 
63
- render(text, render_options)
27
+ pub fn escape_commonmark_link_destination_rs(text: RString) -> Result<String, Error> {
28
+ // SAFETY: `RString::as_str` returns a reference directly to Ruby memory.
29
+ // We do not hold onto or save the `&str`, or otherwise permit Ruby to GC or
30
+ // modify it while in `glfm::escape_commonmark_link_destination`.
31
+ // https://docs.rs/magnus/latest/magnus/r_string/struct.RString.html#method.as_str
32
+ Ok(escape_commonmark_link_destination(unsafe {
33
+ text.as_str()?
34
+ }))
64
35
  }
65
36
 
66
37
  #[magnus::init]
@@ -68,6 +39,14 @@ fn init() -> Result<(), Error> {
68
39
  let module = define_module("GLFMMarkdown")?;
69
40
 
70
41
  module.define_singleton_method("render_to_html_rs", function!(render_to_html_rs, 2))?;
42
+ module.define_singleton_method(
43
+ "escape_commonmark_inline_rs",
44
+ function!(escape_commonmark_inline_rs, 1),
45
+ )?;
46
+ module.define_singleton_method(
47
+ "escape_commonmark_link_destination_rs",
48
+ function!(escape_commonmark_link_destination_rs, 1),
49
+ )?;
71
50
 
72
51
  Ok(())
73
52
  }
@@ -1,10 +1,12 @@
1
- mod glfm;
2
- use glfm::{render, RenderOptions};
3
1
  use std::io::Read;
4
2
  use std::io::Write;
5
3
 
6
4
  use clap::Parser;
7
5
 
6
+ mod glfm;
7
+ use glfm::{render, RenderOptions};
8
+ mod formatter;
9
+
8
10
  #[derive(Parser, Debug)]
9
11
  #[command(author, version, about, long_about = None)]
10
12
  struct Args {
@@ -70,7 +72,7 @@ struct Args {
70
72
  #[arg(long)]
71
73
  hardbreaks: bool,
72
74
 
73
- /// Enable the 'header IDs` extension, with the given ID prefix
75
+ /// Enable the 'header IDs' extension, with the given ID prefix
74
76
  #[arg(long, value_name = "PREFIX")]
75
77
  header_ids: Option<String>,
76
78
 
@@ -148,7 +150,7 @@ struct Args {
148
150
 
149
151
  /// Output classes on tasklist elements so that they can be styled with CSS
150
152
  #[arg(long)]
151
- tasklist_clases: bool,
153
+ tasklist_classes: bool,
152
154
 
153
155
  /// Enables underlines using double underscores
154
156
  #[arg(long)]
@@ -181,6 +183,11 @@ struct Args {
181
183
  #[arg(long)]
182
184
  placeholder_detection: bool,
183
185
 
186
+ /// When the 'escaped_char_spans' render option is enabled, only emit `<span
187
+ /// data-escaped-char>` around the characters in the supplied string.
188
+ #[arg(long)]
189
+ only_escape_chars: Option<String>,
190
+
184
191
  /// Show debug information
185
192
  #[arg(long)]
186
193
  debug: bool,
@@ -237,16 +244,17 @@ fn main() {
237
244
  tasklist: cli.tasklist,
238
245
  tasklist_classes: cli.tasklist_classes,
239
246
  underline: cli.underline,
240
- unsafe_: cli.unsafe_,
247
+ r#unsafe: cli.unsafe_,
241
248
  wikilinks_title_after_pipe: cli.wikilinks_title_after_pipe,
242
249
  wikilinks_title_before_pipe: cli.wikilinks_title_before_pipe,
243
250
 
244
251
  default_html: cli.default_html,
245
252
  placeholder_detection: cli.placeholder_detection,
253
+ only_escape_chars: cli.only_escape_chars.map(|s| s.chars().collect()),
246
254
  debug: cli.debug,
247
255
  };
248
256
 
249
- let result = render(source.to_string(), options);
257
+ let result = render(&source, &options);
250
258
 
251
259
  if let Some(output_filename) = cli.output {
252
260
  std::fs::write(output_filename, &result).unwrap();
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GLFMMarkdown
4
- VERSION = '0.0.33'
4
+ VERSION = '0.0.35'
5
5
  end
data/lib/glfm_markdown.rb CHANGED
@@ -34,7 +34,7 @@ module GLFMMarkdown
34
34
  class << self
35
35
  def to_html(markdown, options: {})
36
36
  raise TypeError, 'markdown must be a String' unless markdown.is_a?(String)
37
- raise TypeError, 'markdown must be UTF-8 encoded' unless markdown.encoding.name == "UTF-8"
37
+ raise TypeError, 'markdown must be UTF-8 encoded' unless markdown.encoding == Encoding::UTF_8
38
38
  raise TypeError, 'options must be a Hash' unless options.is_a?(Hash)
39
39
 
40
40
  default_options = options[:glfm] ? GLFM_DEFAULT_OPTIONS : {}
@@ -44,5 +44,19 @@ module GLFMMarkdown
44
44
 
45
45
  render_to_html_rs(markdown, default_options.merge(options))
46
46
  end
47
+
48
+ def escape_commonmark_inline(text)
49
+ raise TypeError, 'text must be a String' unless text.is_a?(String)
50
+ raise TypeError, 'text must be UTF-8 encoded' unless text.encoding == Encoding::UTF_8
51
+
52
+ escape_commonmark_inline_rs(text)
53
+ end
54
+
55
+ def escape_commonmark_link_destination(url)
56
+ raise TypeError, 'url must be a String' unless url.is_a?(String)
57
+ raise TypeError, 'url must be UTF-8 encoded' unless url.encoding == Encoding::UTF_8
58
+
59
+ escape_commonmark_link_destination_rs(url)
60
+ end
47
61
  end
48
62
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-glfm-markdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.33
4
+ version: 0.0.35
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Brett Walker
8
+ - Asherah Connor
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2025-07-23 00:00:00.000000000 Z
12
+ date: 2025-09-30 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rb_sys
@@ -55,6 +56,7 @@ dependencies:
55
56
  description: Markdown processing for GitLab Flavored Markdown
56
57
  email:
57
58
  - bwalker@gitlab.com
59
+ - aconnor@gitlab.com
58
60
  executables: []
59
61
  extensions: []
60
62
  extra_rdoc_files: []
@@ -65,6 +67,7 @@ files:
65
67
  - ext/glfm_markdown/Cargo.lock
66
68
  - ext/glfm_markdown/Cargo.toml
67
69
  - ext/glfm_markdown/extconf.rb
70
+ - ext/glfm_markdown/src/formatter.rs
68
71
  - ext/glfm_markdown/src/glfm.rs
69
72
  - ext/glfm_markdown/src/lib.rs
70
73
  - ext/glfm_markdown/src/main.rs