gitlab-glfm-markdown 0.0.34-x86_64-linux-gnu → 0.0.35-x86_64-linux-gnu
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 +4 -4
- data/Cargo.lock +22 -3
- data/README.md +3 -1
- data/ext/glfm_markdown/Cargo.lock +22 -3
- data/ext/glfm_markdown/Cargo.toml +4 -2
- data/ext/glfm_markdown/src/formatter.rs +408 -0
- data/ext/glfm_markdown/src/glfm.rs +20 -361
- data/ext/glfm_markdown/src/lib.rs +34 -55
- data/ext/glfm_markdown/src/main.rs +14 -6
- data/lib/glfm_markdown/3.1/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.2/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.3/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.4/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/version.rb +1 -1
- data/lib/glfm_markdown.rb +15 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a0bfd8ee58630516f18c17976d4a31196ee9e0f848f8c6965705f9332b29c84
|
4
|
+
data.tar.gz: e416ba186a3a6c946bbd7cd0be447dccbe6f6160528bfeb08e2b823916ebc9fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2890b1bd9a951b1167b8568b3e61165cc2acd58a87a1ae56c01b1e96550616026d5d8dae1607b64f74f7d75f2c1826140e68815783abe4067868aaf351963ce
|
7
|
+
data.tar.gz: 803a56782f73bd83dc1b382d635285d5ac090e85f11ec21dc82890415805b8ce7144d3290d501eb3959c2b1f00884c0cd69bfe4a6684462660cea5589c577448
|
data/Cargo.lock
CHANGED
@@ -227,9 +227,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
|
227
227
|
|
228
228
|
[[package]]
|
229
229
|
name = "comrak"
|
230
|
-
version = "0.
|
230
|
+
version = "0.43.0"
|
231
231
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
232
|
-
checksum = "
|
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.
|
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.
|
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
|
|
@@ -227,9 +227,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
|
227
227
|
|
228
228
|
[[package]]
|
229
229
|
name = "comrak"
|
230
|
-
version = "0.
|
230
|
+
version = "0.43.0"
|
231
231
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
232
|
-
checksum = "
|
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.
|
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,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "glfm_markdown"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.35"
|
4
4
|
edition = "2021"
|
5
5
|
authors = ["digitalmoksha <bwalker@gitlab.com>", "Asherah Connor <aconnor@gitlab.com>"]
|
6
6
|
description = "GitLab Flavored Markdown parser and formatter. 100% CommonMark-compatible. Experimental."
|
@@ -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.
|
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
|
1
|
+
use comrak::{parse_document, Arena, Plugins};
|
2
|
+
use serde::Deserialize;
|
2
3
|
|
3
|
-
use
|
4
|
-
use comrak::nodes::{AstNode, ListType, NodeValue};
|
5
|
-
use comrak::{create_formatter, html, parse_document, Arena, Plugins};
|
6
|
-
use lazy_static::lazy_static;
|
7
|
-
use regex::Regex;
|
4
|
+
use crate::formatter::{CustomFormatter, RenderUserData};
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
}
|
12
|
-
|
13
|
-
#[derive(Debug)]
|
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
|
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
|
-
|
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.
|
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,356 +126,23 @@ impl From<&RenderOptions> for comrak::Options<'_> {
|
|
134
126
|
}
|
135
127
|
}
|
136
128
|
|
137
|
-
pub fn render(text:
|
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:
|
142
|
-
let user_data = RenderUserData::from(
|
143
|
-
let options = comrak::Options::from(
|
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(
|
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,
|
142
|
+
let root = parse_document(&arena, text, &options);
|
151
143
|
|
152
|
-
let mut
|
153
|
-
CustomFormatter::format_document_with_plugins(root, &options, &mut
|
144
|
+
let mut out = String::new();
|
145
|
+
CustomFormatter::format_document_with_plugins(root, &options, &mut out, plugins, user_data)
|
154
146
|
.unwrap();
|
155
|
-
|
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
|
-
NodeValue::Heading(_) => |context, node, entering| {
|
182
|
-
return render_heading(context, node, entering);
|
183
|
-
},
|
184
|
-
});
|
185
|
-
|
186
|
-
fn render_image<'a>(
|
187
|
-
context: &mut Context<RenderUserData>,
|
188
|
-
node: &'a AstNode<'a>,
|
189
|
-
entering: bool,
|
190
|
-
) -> io::Result<ChildRendering> {
|
191
|
-
let NodeValue::Image(ref nl) = node.data.borrow().value else {
|
192
|
-
unreachable!()
|
193
|
-
};
|
194
|
-
|
195
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
|
196
|
-
return html::format_node_default(context, node, entering);
|
197
|
-
}
|
198
|
-
|
199
|
-
if entering {
|
200
|
-
if context.options.render.figure_with_caption {
|
201
|
-
context.write_all(b"<figure>")?;
|
202
|
-
}
|
203
|
-
context.write_all(b"<img")?;
|
204
|
-
html::render_sourcepos(context, node)?;
|
205
|
-
context.write_all(b" src=\"")?;
|
206
|
-
let url = nl.url.as_bytes();
|
207
|
-
if context.options.render.unsafe_ || !html::dangerous_url(url) {
|
208
|
-
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
|
209
|
-
context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
|
210
|
-
} else {
|
211
|
-
context.escape_href(url)?;
|
212
|
-
}
|
213
|
-
}
|
214
|
-
|
215
|
-
context.write_all(b"\"")?;
|
216
|
-
|
217
|
-
if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
|
218
|
-
context.write_all(b" data-placeholder")?;
|
219
|
-
}
|
220
|
-
|
221
|
-
context.write_all(b" alt=\"")?;
|
222
|
-
|
223
|
-
return Ok(ChildRendering::Plain);
|
224
|
-
} else {
|
225
|
-
if !nl.title.is_empty() {
|
226
|
-
context.write_all(b"\" title=\"")?;
|
227
|
-
context.escape(nl.title.as_bytes())?;
|
228
|
-
}
|
229
|
-
context.write_all(b"\" />")?;
|
230
|
-
if context.options.render.figure_with_caption {
|
231
|
-
if !nl.title.is_empty() {
|
232
|
-
context.write_all(b"<figcaption>")?;
|
233
|
-
context.escape(nl.title.as_bytes())?;
|
234
|
-
context.write_all(b"</figcaption>")?;
|
235
|
-
}
|
236
|
-
context.write_all(b"</figure>")?;
|
237
|
-
};
|
238
|
-
}
|
239
|
-
|
240
|
-
Ok(ChildRendering::HTML)
|
241
|
-
}
|
242
|
-
|
243
|
-
fn render_link<'a>(
|
244
|
-
context: &mut Context<RenderUserData>,
|
245
|
-
node: &'a AstNode<'a>,
|
246
|
-
entering: bool,
|
247
|
-
) -> io::Result<ChildRendering> {
|
248
|
-
let NodeValue::Link(ref nl) = node.data.borrow().value else {
|
249
|
-
unreachable!()
|
250
|
-
};
|
251
|
-
|
252
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
|
253
|
-
return html::format_node_default(context, node, entering);
|
254
|
-
}
|
255
|
-
|
256
|
-
let parent_node = node.parent();
|
257
|
-
|
258
|
-
if !context.options.parse.relaxed_autolinks
|
259
|
-
|| (parent_node.is_none()
|
260
|
-
|| !matches!(
|
261
|
-
parent_node.unwrap().data.borrow().value,
|
262
|
-
NodeValue::Link(..)
|
263
|
-
))
|
264
|
-
{
|
265
|
-
if entering {
|
266
|
-
context.write_all(b"<a")?;
|
267
|
-
html::render_sourcepos(context, node)?;
|
268
|
-
context.write_all(b" href=\"")?;
|
269
|
-
let url = nl.url.as_bytes();
|
270
|
-
if context.options.render.unsafe_ || !html::dangerous_url(url) {
|
271
|
-
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
272
|
-
context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
|
273
|
-
} else {
|
274
|
-
context.escape_href(url)?;
|
275
|
-
}
|
276
|
-
}
|
277
|
-
context.write_all(b"\"")?;
|
278
|
-
|
279
|
-
if !nl.title.is_empty() {
|
280
|
-
context.write_all(b" title=\"")?;
|
281
|
-
context.escape(nl.title.as_bytes())?;
|
282
|
-
}
|
283
|
-
|
284
|
-
if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
|
285
|
-
context.write_all(b" data-placeholder")?;
|
286
|
-
}
|
287
|
-
|
288
|
-
context.write_all(b">")?;
|
289
|
-
} else {
|
290
|
-
context.write_all(b"</a>")?;
|
291
|
-
}
|
292
|
-
}
|
293
|
-
|
294
|
-
Ok(ChildRendering::HTML)
|
295
|
-
}
|
296
|
-
|
297
|
-
// Overridden to use class `task-list` instead of `contains-task-list`
|
298
|
-
// to align with GitLab class usage
|
299
|
-
fn render_list<'a>(
|
300
|
-
context: &mut Context<RenderUserData>,
|
301
|
-
node: &'a AstNode<'a>,
|
302
|
-
entering: bool,
|
303
|
-
) -> io::Result<ChildRendering> {
|
304
|
-
if !entering || !context.options.render.tasklist_classes {
|
305
|
-
return html::format_node_default(context, node, entering);
|
306
|
-
}
|
307
|
-
|
308
|
-
let NodeValue::List(ref nl) = node.data.borrow().value else {
|
309
|
-
unreachable!()
|
310
|
-
};
|
311
|
-
|
312
|
-
context.cr()?;
|
313
|
-
match nl.list_type {
|
314
|
-
ListType::Bullet => {
|
315
|
-
context.write_all(b"<ul")?;
|
316
|
-
if nl.is_task_list {
|
317
|
-
context.write_all(b" class=\"task-list\"")?;
|
318
|
-
}
|
319
|
-
html::render_sourcepos(context, node)?;
|
320
|
-
context.write_all(b">\n")?;
|
321
|
-
}
|
322
|
-
ListType::Ordered => {
|
323
|
-
context.write_all(b"<ol")?;
|
324
|
-
if nl.is_task_list {
|
325
|
-
context.write_all(b" class=\"task-list\"")?;
|
326
|
-
}
|
327
|
-
html::render_sourcepos(context, node)?;
|
328
|
-
if nl.start == 1 {
|
329
|
-
context.write_all(b">\n")?;
|
330
|
-
} else {
|
331
|
-
writeln!(context, " start=\"{}\">", nl.start)?;
|
332
|
-
}
|
333
|
-
}
|
334
|
-
}
|
335
|
-
|
336
|
-
Ok(ChildRendering::HTML)
|
337
|
-
}
|
338
|
-
|
339
|
-
// Overridden to detect inapplicable task list items
|
340
|
-
fn render_task_item<'a>(
|
341
|
-
context: &mut Context<RenderUserData>,
|
342
|
-
node: &'a AstNode<'a>,
|
343
|
-
entering: bool,
|
344
|
-
) -> io::Result<ChildRendering> {
|
345
|
-
if !context.user.inapplicable_tasks {
|
346
|
-
return html::format_node_default(context, node, entering);
|
347
|
-
}
|
348
|
-
|
349
|
-
let NodeValue::TaskItem(symbol) = node.data.borrow().value else {
|
350
|
-
unreachable!()
|
351
|
-
};
|
352
|
-
|
353
|
-
if symbol.is_none() || matches!(symbol, Some('x' | 'X')) {
|
354
|
-
return html::format_node_default(context, node, entering);
|
355
|
-
}
|
356
|
-
|
357
|
-
if entering {
|
358
|
-
// Handle an inapplicable task symbol.
|
359
|
-
if matches!(symbol, Some('~')) {
|
360
|
-
context.cr()?;
|
361
|
-
context.write_all(b"<li")?;
|
362
|
-
context.write_all(b" class=\"inapplicable")?;
|
363
|
-
|
364
|
-
if context.options.render.tasklist_classes {
|
365
|
-
context.write_all(b" task-list-item")?;
|
366
|
-
}
|
367
|
-
context.write_all(b"\"")?;
|
368
|
-
|
369
|
-
html::render_sourcepos(context, node)?;
|
370
|
-
context.write_all(b">")?;
|
371
|
-
context.write_all(b"<input type=\"checkbox\"")?;
|
372
|
-
|
373
|
-
if context.options.render.tasklist_classes {
|
374
|
-
context.write_all(b" class=\"task-list-item-checkbox\"")?;
|
375
|
-
}
|
376
|
-
|
377
|
-
context.write_all(b" data-inapplicable disabled=\"\"> ")?;
|
378
|
-
} else {
|
379
|
-
// Don't allow unsupported symbols to render a checkbox
|
380
|
-
context.cr()?;
|
381
|
-
context.write_all(b"<li")?;
|
382
|
-
|
383
|
-
if context.options.render.tasklist_classes {
|
384
|
-
context.write_all(b" class=\"task-list-item\"")?;
|
385
|
-
}
|
386
|
-
|
387
|
-
html::render_sourcepos(context, node)?;
|
388
|
-
context.write_all(b">")?;
|
389
|
-
context.write_all(b"[")?;
|
390
|
-
context.escape(symbol.unwrap().to_string().as_bytes())?;
|
391
|
-
context.write_all(b"] ")?;
|
392
|
-
}
|
393
|
-
} else {
|
394
|
-
context.write_all(b"</li>\n")?;
|
395
|
-
}
|
396
|
-
|
397
|
-
Ok(ChildRendering::HTML)
|
398
|
-
}
|
399
|
-
|
400
|
-
fn render_text<'a>(
|
401
|
-
context: &mut Context<RenderUserData>,
|
402
|
-
node: &'a AstNode<'a>,
|
403
|
-
entering: bool,
|
404
|
-
) -> io::Result<ChildRendering> {
|
405
|
-
let NodeValue::Text(ref literal) = node.data.borrow().value else {
|
406
|
-
unreachable!()
|
407
|
-
};
|
408
|
-
|
409
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
|
410
|
-
return html::format_node_default(context, node, entering);
|
411
|
-
}
|
412
|
-
|
413
|
-
// Don't currently support placeholders in the text inside links or images.
|
414
|
-
// If the text has an underscore in it, then the parser will not combine
|
415
|
-
// the multiple text nodes in `comrak`'s `postprocess_text_nodes`, breaking up
|
416
|
-
// the placeholder into multiple text nodes.
|
417
|
-
// For example, `[%{a_b}](link)`.
|
418
|
-
let parent = node.parent().unwrap();
|
419
|
-
if matches!(
|
420
|
-
parent.data.borrow().value,
|
421
|
-
NodeValue::Link(_) | NodeValue::Image(_)
|
422
|
-
) {
|
423
|
-
return html::format_node_default(context, node, entering);
|
424
|
-
}
|
425
|
-
|
426
|
-
if entering {
|
427
|
-
let mut cursor: usize = 0;
|
428
|
-
|
429
|
-
for mat in PLACEHOLDER_REGEX.find_iter(literal) {
|
430
|
-
if mat.start() > cursor {
|
431
|
-
context.escape(literal[cursor..mat.start()].as_bytes())?;
|
432
|
-
}
|
433
|
-
|
434
|
-
context.write_all(b"<span data-placeholder>")?;
|
435
|
-
context.escape(literal[mat.start()..mat.end()].as_bytes())?;
|
436
|
-
context.write_all(b"</span>")?;
|
437
|
-
|
438
|
-
cursor = mat.end();
|
439
|
-
}
|
440
|
-
|
441
|
-
if cursor < literal.len() {
|
442
|
-
context.escape(literal[cursor..literal.len()].as_bytes())?;
|
443
|
-
}
|
444
|
-
}
|
445
|
-
|
446
|
-
Ok(ChildRendering::HTML)
|
447
|
-
}
|
448
|
-
|
449
|
-
fn render_heading<'a>(
|
450
|
-
context: &mut Context<RenderUserData>,
|
451
|
-
node: &'a AstNode<'a>,
|
452
|
-
entering: bool,
|
453
|
-
) -> io::Result<ChildRendering> {
|
454
|
-
let NodeValue::Heading(ref nh) = node.data.borrow().value else {
|
455
|
-
unreachable!()
|
456
|
-
};
|
457
|
-
|
458
|
-
match context.plugins.render.heading_adapter {
|
459
|
-
None => {
|
460
|
-
// Overrides the default handling in order to render the heading text
|
461
|
-
// inside the anchor tag to better support screen readers
|
462
|
-
if entering {
|
463
|
-
context.cr()?;
|
464
|
-
write!(context, "<h{}", nh.level)?;
|
465
|
-
render_sourcepos(context, node)?;
|
466
|
-
context.write_all(b">")?;
|
467
|
-
|
468
|
-
if let Some(ref prefix) = context.options.extension.header_ids {
|
469
|
-
let mut text_content = Vec::with_capacity(20);
|
470
|
-
collect_text(node, &mut text_content);
|
471
|
-
|
472
|
-
let mut id = String::from_utf8(text_content).unwrap();
|
473
|
-
id = context.anchorizer.anchorize(id);
|
474
|
-
write!(
|
475
|
-
context,
|
476
|
-
r##"<a href="#{id}" class="anchor" id="{prefix}{id}">"##
|
477
|
-
)?;
|
478
|
-
}
|
479
|
-
} else {
|
480
|
-
if context.options.extension.header_ids.is_some() {
|
481
|
-
write!(context, "</a>")?;
|
482
|
-
}
|
483
|
-
writeln!(context, "</h{}>", nh.level)?;
|
484
|
-
}
|
485
|
-
Ok(ChildRendering::HTML)
|
486
|
-
}
|
487
|
-
Some(_adapter) => format_node_default(context, node, entering),
|
488
|
-
}
|
147
|
+
out
|
489
148
|
}
|
@@ -1,66 +1,37 @@
|
|
1
|
-
use
|
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
|
-
|
7
|
-
|
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
|
-
|
13
|
-
|
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
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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();
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
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
|
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,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-glfm-markdown
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.35
|
5
5
|
platform: x86_64-linux-gnu
|
6
6
|
authors:
|
7
7
|
- Brett Walker
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-09-
|
12
|
+
date: 2025-09-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rb_sys
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- ext/glfm_markdown/Cargo.lock
|
68
68
|
- ext/glfm_markdown/Cargo.toml
|
69
69
|
- ext/glfm_markdown/extconf.rb
|
70
|
+
- ext/glfm_markdown/src/formatter.rs
|
70
71
|
- ext/glfm_markdown/src/glfm.rs
|
71
72
|
- ext/glfm_markdown/src/lib.rs
|
72
73
|
- ext/glfm_markdown/src/main.rs
|