gitlab-glfm-markdown 0.0.37-aarch64-linux-musl → 0.0.39-aarch64-linux-musl
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 +24 -111
- data/README.md +14 -13
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/Cargo.lock +24 -111
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/Cargo.toml +7 -7
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/extconf.rb +1 -1
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/formatter.rs +224 -155
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/glfm.rs +21 -35
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/lib.rs +5 -5
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/main.rs +47 -44
- data/lib/{glfm_markdown.rb → gitlab-glfm-markdown.rb} +3 -5
- data/lib/gitlab_glfm_markdown/3.1/gitlab_glfm_markdown.so +0 -0
- data/lib/gitlab_glfm_markdown/3.2/gitlab_glfm_markdown.so +0 -0
- data/lib/gitlab_glfm_markdown/3.3/gitlab_glfm_markdown.so +0 -0
- data/lib/gitlab_glfm_markdown/3.4/gitlab_glfm_markdown.so +0 -0
- data/lib/{glfm_markdown → gitlab_glfm_markdown}/loader.rb +2 -2
- data/lib/{glfm_markdown → gitlab_glfm_markdown}/version.rb +1 -1
- metadata +16 -16
- 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
|
@@ -1,104 +1,99 @@
|
|
|
1
1
|
use std::fmt::{self, Write};
|
|
2
|
+
use std::sync::LazyLock;
|
|
2
3
|
|
|
3
|
-
use comrak::html::{render_sourcepos, ChildRendering, Context};
|
|
4
|
-
use comrak::nodes::{AstNode, ListType, NodeValue};
|
|
5
|
-
use comrak::{create_formatter, html};
|
|
6
|
-
use lazy_static::lazy_static;
|
|
4
|
+
use comrak::html::{collect_text, format_node_default, render_sourcepos, ChildRendering, Context};
|
|
5
|
+
use comrak::nodes::{AstNode, ListType, NodeHeading, NodeLink, NodeList, NodeValue};
|
|
6
|
+
use comrak::{create_formatter, html, node_matches};
|
|
7
7
|
use regex::Regex;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
9
|
+
use crate::glfm::RenderOptions;
|
|
10
|
+
|
|
11
|
+
static PLACEHOLDER_REGEX: LazyLock<Regex> =
|
|
12
|
+
LazyLock::new(|| Regex::new(r"%\{(?:\w{1,30})}").unwrap());
|
|
14
13
|
|
|
15
14
|
pub struct RenderUserData {
|
|
16
15
|
pub default_html: bool,
|
|
17
16
|
pub inapplicable_tasks: bool,
|
|
18
17
|
pub placeholder_detection: bool,
|
|
19
18
|
pub only_escape_chars: Option<Vec<char>>,
|
|
20
|
-
pub
|
|
19
|
+
pub header_accessibility: bool,
|
|
20
|
+
|
|
21
|
+
last_heading: Option<String>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl From<&RenderOptions> for RenderUserData {
|
|
25
|
+
fn from(options: &RenderOptions) -> Self {
|
|
26
|
+
RenderUserData {
|
|
27
|
+
default_html: options.default_html,
|
|
28
|
+
inapplicable_tasks: options.inapplicable_tasks,
|
|
29
|
+
placeholder_detection: options.placeholder_detection,
|
|
30
|
+
only_escape_chars: options.only_escape_chars.clone(),
|
|
31
|
+
header_accessibility: options.header_accessibility,
|
|
32
|
+
last_heading: None,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
// The important thing to remember is that this overrides the default behavior of the
|
|
24
38
|
// specified nodes. If we do override a node, then it's our responsibility to ensure that
|
|
25
|
-
// any changes in the
|
|
39
|
+
// any changes in the Comrak code for those nodes is backported to here, such as when
|
|
26
40
|
// `figcaption` support was added.
|
|
41
|
+
//
|
|
27
42
|
// One idea to limit that would be having the ability to specify attributes that would
|
|
28
43
|
// 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
|
|
44
|
+
// inject the changes we need. Such a feature would need to be added to Comrak.
|
|
30
45
|
create_formatter!(CustomFormatter<RenderUserData>, {
|
|
31
|
-
NodeValue::Text(
|
|
32
|
-
return render_text(context, node, entering);
|
|
46
|
+
NodeValue::Text(ref literal) => |context, node, entering| {
|
|
47
|
+
return render_text(context, node, entering, literal);
|
|
33
48
|
},
|
|
34
|
-
NodeValue::Link(
|
|
35
|
-
return render_link(context, node, entering);
|
|
49
|
+
NodeValue::Link(ref nl) => |context, node, entering| {
|
|
50
|
+
return render_link(context, node, entering, nl);
|
|
36
51
|
},
|
|
37
|
-
NodeValue::Image(
|
|
38
|
-
return render_image(context, node, entering);
|
|
52
|
+
NodeValue::Image(ref nl) => |context, node, entering| {
|
|
53
|
+
return render_image(context, node, entering, nl);
|
|
39
54
|
},
|
|
40
|
-
NodeValue::List(
|
|
41
|
-
return render_list(context, node, entering);
|
|
55
|
+
NodeValue::List(ref nl) => |context, node, entering| {
|
|
56
|
+
return render_list(context, node, entering, nl);
|
|
42
57
|
},
|
|
43
|
-
NodeValue::TaskItem(
|
|
44
|
-
return render_task_item(context, node, entering);
|
|
58
|
+
NodeValue::TaskItem(symbol) => |context, node, entering| {
|
|
59
|
+
return render_task_item(context, node, entering, symbol);
|
|
45
60
|
},
|
|
46
61
|
NodeValue::Escaped => |context, node, entering| {
|
|
47
62
|
return render_escaped(context, node, entering);
|
|
48
63
|
},
|
|
64
|
+
NodeValue::Heading(ref nh) => |context, node, entering| {
|
|
65
|
+
return render_heading(context, node, entering, nh);
|
|
66
|
+
},
|
|
49
67
|
});
|
|
50
68
|
|
|
51
|
-
fn
|
|
69
|
+
fn render_text<'a>(
|
|
52
70
|
context: &mut Context<RenderUserData>,
|
|
53
71
|
node: &'a AstNode<'a>,
|
|
54
72
|
entering: bool,
|
|
73
|
+
literal: &str,
|
|
55
74
|
) -> Result<ChildRendering, fmt::Error> {
|
|
56
|
-
|
|
57
|
-
unreachable!()
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
75
|
+
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
|
|
61
76
|
return html::format_node_default(context, node, entering);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
if entering {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
context.write_str(" src=\"")?;
|
|
71
|
-
if context.options.render.unsafe_ || !html::dangerous_url(&nl.url) {
|
|
72
|
-
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
|
|
73
|
-
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
74
|
-
} else {
|
|
75
|
-
context.escape_href(&nl.url)?;
|
|
80
|
+
let mut cursor: usize = 0;
|
|
81
|
+
|
|
82
|
+
for mat in PLACEHOLDER_REGEX.find_iter(literal) {
|
|
83
|
+
if mat.start() > cursor {
|
|
84
|
+
context.escape(&literal[cursor..mat.start()])?;
|
|
76
85
|
}
|
|
77
|
-
}
|
|
78
86
|
|
|
79
|
-
|
|
87
|
+
context.write_str("<span data-placeholder>")?;
|
|
88
|
+
context.escape(&literal[mat.start()..mat.end()])?;
|
|
89
|
+
context.write_str("</span>")?;
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
context.write_str(" data-placeholder")?;
|
|
91
|
+
cursor = mat.end();
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return Ok(ChildRendering::Plain);
|
|
88
|
-
} else {
|
|
89
|
-
if !nl.title.is_empty() {
|
|
90
|
-
context.write_str("\" title=\"")?;
|
|
91
|
-
context.escape(&nl.title)?;
|
|
94
|
+
if cursor < literal.len() {
|
|
95
|
+
context.escape(&literal[cursor..])?;
|
|
92
96
|
}
|
|
93
|
-
context.write_str("\" />")?;
|
|
94
|
-
if context.options.render.figure_with_caption {
|
|
95
|
-
if !nl.title.is_empty() {
|
|
96
|
-
context.write_str("<figcaption>")?;
|
|
97
|
-
context.escape(&nl.title)?;
|
|
98
|
-
context.write_str("</figcaption>")?;
|
|
99
|
-
}
|
|
100
|
-
context.write_str("</figure>")?;
|
|
101
|
-
};
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
Ok(ChildRendering::HTML)
|
|
@@ -108,11 +103,8 @@ fn render_link<'a>(
|
|
|
108
103
|
context: &mut Context<RenderUserData>,
|
|
109
104
|
node: &'a AstNode<'a>,
|
|
110
105
|
entering: bool,
|
|
106
|
+
nl: &NodeLink,
|
|
111
107
|
) -> Result<ChildRendering, fmt::Error> {
|
|
112
|
-
let NodeValue::Link(ref nl) = node.data.borrow().value else {
|
|
113
|
-
unreachable!()
|
|
114
|
-
};
|
|
115
|
-
|
|
116
108
|
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
117
109
|
return html::format_node_default(context, node, entering);
|
|
118
110
|
}
|
|
@@ -130,7 +122,7 @@ fn render_link<'a>(
|
|
|
130
122
|
context.write_str("<a")?;
|
|
131
123
|
html::render_sourcepos(context, node)?;
|
|
132
124
|
context.write_str(" href=\"")?;
|
|
133
|
-
if context.options.render.
|
|
125
|
+
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
134
126
|
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
|
135
127
|
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
136
128
|
} else {
|
|
@@ -144,9 +136,8 @@ fn render_link<'a>(
|
|
|
144
136
|
context.escape(&nl.title)?;
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
}
|
|
139
|
+
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
140
|
+
context.write_str(" data-placeholder")?;
|
|
150
141
|
|
|
151
142
|
context.write_str(">")?;
|
|
152
143
|
} else {
|
|
@@ -157,21 +148,70 @@ fn render_link<'a>(
|
|
|
157
148
|
Ok(ChildRendering::HTML)
|
|
158
149
|
}
|
|
159
150
|
|
|
151
|
+
fn render_image<'a>(
|
|
152
|
+
context: &mut Context<RenderUserData>,
|
|
153
|
+
node: &'a AstNode<'a>,
|
|
154
|
+
entering: bool,
|
|
155
|
+
nl: &NodeLink,
|
|
156
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
157
|
+
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
158
|
+
return html::format_node_default(context, node, entering);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if entering {
|
|
162
|
+
if context.options.render.figure_with_caption {
|
|
163
|
+
context.write_str("<figure>")?;
|
|
164
|
+
}
|
|
165
|
+
context.write_str("<img")?;
|
|
166
|
+
html::render_sourcepos(context, node)?;
|
|
167
|
+
context.write_str(" src=\"")?;
|
|
168
|
+
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
169
|
+
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
|
|
170
|
+
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
171
|
+
} else {
|
|
172
|
+
context.escape_href(&nl.url)?;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
context.write_str("\"")?;
|
|
177
|
+
|
|
178
|
+
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
179
|
+
context.write_str(" data-placeholder")?;
|
|
180
|
+
|
|
181
|
+
context.write_str(" alt=\"")?;
|
|
182
|
+
|
|
183
|
+
return Ok(ChildRendering::Plain);
|
|
184
|
+
} else {
|
|
185
|
+
if !nl.title.is_empty() {
|
|
186
|
+
context.write_str("\" title=\"")?;
|
|
187
|
+
context.escape(&nl.title)?;
|
|
188
|
+
}
|
|
189
|
+
context.write_str("\" />")?;
|
|
190
|
+
if context.options.render.figure_with_caption {
|
|
191
|
+
if !nl.title.is_empty() {
|
|
192
|
+
context.write_str("<figcaption>")?;
|
|
193
|
+
context.escape(&nl.title)?;
|
|
194
|
+
context.write_str("</figcaption>")?;
|
|
195
|
+
}
|
|
196
|
+
context.write_str("</figure>")?;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Ok(ChildRendering::HTML)
|
|
201
|
+
}
|
|
202
|
+
|
|
160
203
|
// Overridden to use class `task-list` instead of `contains-task-list`
|
|
161
204
|
// to align with GitLab class usage
|
|
162
205
|
fn render_list<'a>(
|
|
163
206
|
context: &mut Context<RenderUserData>,
|
|
164
207
|
node: &'a AstNode<'a>,
|
|
165
208
|
entering: bool,
|
|
209
|
+
nl: &NodeList,
|
|
166
210
|
) -> Result<ChildRendering, fmt::Error> {
|
|
167
211
|
if !entering || !context.options.render.tasklist_classes {
|
|
168
212
|
return html::format_node_default(context, node, entering);
|
|
169
213
|
}
|
|
170
214
|
|
|
171
|
-
let NodeValue::List(ref nl) = node.data.borrow().value else {
|
|
172
|
-
unreachable!()
|
|
173
|
-
};
|
|
174
|
-
|
|
175
215
|
context.cr()?;
|
|
176
216
|
match nl.list_type {
|
|
177
217
|
ListType::Bullet => {
|
|
@@ -204,143 +244,172 @@ fn render_task_item<'a>(
|
|
|
204
244
|
context: &mut Context<RenderUserData>,
|
|
205
245
|
node: &'a AstNode<'a>,
|
|
206
246
|
entering: bool,
|
|
247
|
+
symbol: Option<char>,
|
|
207
248
|
) -> Result<ChildRendering, fmt::Error> {
|
|
208
249
|
if !context.user.inapplicable_tasks {
|
|
209
250
|
return html::format_node_default(context, node, entering);
|
|
210
251
|
}
|
|
211
252
|
|
|
212
|
-
let
|
|
213
|
-
|
|
253
|
+
let Some(symbol) = symbol else {
|
|
254
|
+
return html::format_node_default(context, node, entering);
|
|
214
255
|
};
|
|
215
256
|
|
|
216
|
-
if symbol
|
|
257
|
+
if symbol == 'x' || symbol == 'X' {
|
|
217
258
|
return html::format_node_default(context, node, entering);
|
|
218
259
|
}
|
|
219
260
|
|
|
261
|
+
// We only proceed past this point if:
|
|
262
|
+
//
|
|
263
|
+
// * inapplicable_tasks is enabled; and,
|
|
264
|
+
// * the symbol is present (the tasklist didn't contain a ' '), and isn't 'x' or 'X'.
|
|
265
|
+
//
|
|
266
|
+
// There are three possibilities remaining:
|
|
267
|
+
//
|
|
268
|
+
// * the symbol is '~': we write out an inapplicable task item.
|
|
269
|
+
// * the symbol is a different Unicode whitespace: we write out an incomplete task item,
|
|
270
|
+
// per Comrak.
|
|
271
|
+
// * the symbol is something else: we write out the source Markdown that would've been entered,
|
|
272
|
+
// to act like a non-match.
|
|
273
|
+
//
|
|
274
|
+
// TODO: have Comrak accept a list of acceptable tasklist symbols instead. :)
|
|
275
|
+
|
|
276
|
+
let write_li = node
|
|
277
|
+
.parent()
|
|
278
|
+
.map(|p| node_matches!(p, NodeValue::List(_)))
|
|
279
|
+
.unwrap_or_default();
|
|
280
|
+
|
|
220
281
|
if entering {
|
|
221
|
-
|
|
222
|
-
if matches!(symbol, Some('~')) {
|
|
282
|
+
if symbol == '~' {
|
|
223
283
|
context.cr()?;
|
|
224
|
-
|
|
225
|
-
|
|
284
|
+
if write_li {
|
|
285
|
+
context.write_str("<li")?;
|
|
286
|
+
context.write_str(" class=\"inapplicable")?;
|
|
226
287
|
|
|
227
|
-
|
|
228
|
-
|
|
288
|
+
if context.options.render.tasklist_classes {
|
|
289
|
+
context.write_str(" task-list-item")?;
|
|
290
|
+
}
|
|
291
|
+
context.write_str("\"")?;
|
|
292
|
+
html::render_sourcepos(context, node)?;
|
|
293
|
+
context.write_str(">")?;
|
|
229
294
|
}
|
|
230
|
-
context.write_str("\"")?;
|
|
231
|
-
|
|
232
|
-
html::render_sourcepos(context, node)?;
|
|
233
|
-
context.write_str(">")?;
|
|
234
295
|
context.write_str("<input type=\"checkbox\"")?;
|
|
235
|
-
|
|
296
|
+
if !write_li {
|
|
297
|
+
html::render_sourcepos(context, node)?;
|
|
298
|
+
}
|
|
236
299
|
if context.options.render.tasklist_classes {
|
|
237
300
|
context.write_str(" class=\"task-list-item-checkbox\"")?;
|
|
238
301
|
}
|
|
239
302
|
|
|
240
303
|
context.write_str(" data-inapplicable disabled=\"\"> ")?;
|
|
241
|
-
} else {
|
|
242
|
-
// Don't allow unsupported symbols to render a checkbox
|
|
304
|
+
} else if symbol.is_whitespace() {
|
|
243
305
|
context.cr()?;
|
|
244
|
-
|
|
245
|
-
|
|
306
|
+
if write_li {
|
|
307
|
+
context.write_str("<li")?;
|
|
308
|
+
if context.options.render.tasklist_classes {
|
|
309
|
+
context.write_str(" class=\"task-list-item\"")?;
|
|
310
|
+
}
|
|
311
|
+
render_sourcepos(context, node)?;
|
|
312
|
+
context.write_str(">")?;
|
|
313
|
+
}
|
|
314
|
+
context.write_str("<input type=\"checkbox\"")?;
|
|
315
|
+
if !write_li {
|
|
316
|
+
render_sourcepos(context, node)?;
|
|
317
|
+
}
|
|
246
318
|
if context.options.render.tasklist_classes {
|
|
247
|
-
context.write_str(" class=\"task-list-item\"")?;
|
|
319
|
+
context.write_str(" class=\"task-list-item-checkbox\"")?;
|
|
320
|
+
}
|
|
321
|
+
context.write_str(" disabled=\"\" /> ")?;
|
|
322
|
+
} else {
|
|
323
|
+
context.cr()?;
|
|
324
|
+
if write_li {
|
|
325
|
+
context.write_str("<li")?;
|
|
326
|
+
if context.options.render.tasklist_classes {
|
|
327
|
+
context.write_str(" class=\"task-list-item\"")?;
|
|
328
|
+
}
|
|
329
|
+
html::render_sourcepos(context, node)?;
|
|
330
|
+
context.write_str(">")?;
|
|
248
331
|
}
|
|
249
|
-
|
|
250
|
-
html::render_sourcepos(context, node)?;
|
|
251
|
-
context.write_str(">")?;
|
|
252
332
|
context.write_str("[")?;
|
|
253
|
-
context.escape(&symbol.
|
|
333
|
+
context.escape(&symbol.to_string())?;
|
|
254
334
|
context.write_str("] ")?;
|
|
255
335
|
}
|
|
256
|
-
} else {
|
|
336
|
+
} else if write_li {
|
|
257
337
|
context.write_str("</li>\n")?;
|
|
258
338
|
}
|
|
259
339
|
|
|
260
340
|
Ok(ChildRendering::HTML)
|
|
261
341
|
}
|
|
262
342
|
|
|
263
|
-
fn
|
|
343
|
+
fn render_escaped<'a>(
|
|
264
344
|
context: &mut Context<RenderUserData>,
|
|
265
345
|
node: &'a AstNode<'a>,
|
|
266
346
|
entering: bool,
|
|
267
347
|
) -> Result<ChildRendering, fmt::Error> {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
|
|
273
|
-
return html::format_node_default(context, node, entering);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Don't currently support placeholders in the text inside links or images.
|
|
277
|
-
// If the text has an underscore in it, then the parser will not combine
|
|
278
|
-
// the multiple text nodes in `comrak`'s `postprocess_text_nodes`, breaking up
|
|
279
|
-
// the placeholder into multiple text nodes.
|
|
280
|
-
// For example, `[%{a_b}](link)`.
|
|
281
|
-
let parent = node.parent().unwrap();
|
|
282
|
-
if matches!(
|
|
283
|
-
parent.data.borrow().value,
|
|
284
|
-
NodeValue::Link(_) | NodeValue::Image(_)
|
|
285
|
-
) {
|
|
286
|
-
return html::format_node_default(context, node, entering);
|
|
348
|
+
if !context.options.render.escaped_char_spans {
|
|
349
|
+
return Ok(ChildRendering::HTML);
|
|
287
350
|
}
|
|
288
351
|
|
|
289
|
-
if
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
352
|
+
if context
|
|
353
|
+
.user
|
|
354
|
+
.only_escape_chars
|
|
355
|
+
.as_ref()
|
|
356
|
+
.map_or(true, |only_escape_chars| {
|
|
357
|
+
with_node_text_content(node, false, |content| {
|
|
358
|
+
content.chars().count() == 1
|
|
359
|
+
&& only_escape_chars.contains(&content.chars().next().unwrap())
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
{
|
|
363
|
+
if entering {
|
|
364
|
+
context.write_str("<span data-escaped-char")?;
|
|
365
|
+
render_sourcepos(context, node)?;
|
|
366
|
+
context.write_str(">")?;
|
|
367
|
+
} else {
|
|
299
368
|
context.write_str("</span>")?;
|
|
300
|
-
|
|
301
|
-
cursor = mat.end();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if cursor < literal.len() {
|
|
305
|
-
context.escape(&literal[cursor..literal.len()])?;
|
|
306
369
|
}
|
|
307
370
|
}
|
|
308
371
|
|
|
309
372
|
Ok(ChildRendering::HTML)
|
|
310
373
|
}
|
|
311
374
|
|
|
312
|
-
fn
|
|
375
|
+
fn render_heading<'a>(
|
|
313
376
|
context: &mut Context<RenderUserData>,
|
|
314
377
|
node: &'a AstNode<'a>,
|
|
315
378
|
entering: bool,
|
|
379
|
+
nh: &NodeHeading,
|
|
316
380
|
) -> Result<ChildRendering, fmt::Error> {
|
|
317
|
-
if !context.
|
|
318
|
-
return
|
|
381
|
+
if !context.user.header_accessibility || context.plugins.render.heading_adapter.is_some() {
|
|
382
|
+
return format_node_default(context, node, entering);
|
|
319
383
|
}
|
|
320
384
|
|
|
321
|
-
if
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let
|
|
327
|
-
context
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.as_ref()
|
|
331
|
-
.unwrap()
|
|
332
|
-
.contains(&c)
|
|
333
|
-
})
|
|
334
|
-
{
|
|
335
|
-
if entering {
|
|
336
|
-
context.write_str("<span data-escaped-char")?;
|
|
337
|
-
render_sourcepos(context, node)?;
|
|
338
|
-
context.write_str(">")?;
|
|
339
|
-
} else {
|
|
340
|
-
context.write_str("</span>")?;
|
|
385
|
+
if entering {
|
|
386
|
+
context.cr()?;
|
|
387
|
+
write!(context, "<h{}", nh.level)?;
|
|
388
|
+
|
|
389
|
+
if let Some(ref prefix) = context.options.extension.header_ids {
|
|
390
|
+
let text_content = collect_text(node);
|
|
391
|
+
let id = context.anchorizer.anchorize(&text_content);
|
|
392
|
+
write!(context, r##" id="{prefix}{id}""##)?;
|
|
393
|
+
context.user.last_heading = Some(id);
|
|
341
394
|
}
|
|
342
|
-
}
|
|
343
395
|
|
|
396
|
+
render_sourcepos(context, node)?;
|
|
397
|
+
context.write_str(">")?;
|
|
398
|
+
} else {
|
|
399
|
+
if context.options.extension.header_ids.is_some() {
|
|
400
|
+
let id = context.user.last_heading.take().unwrap();
|
|
401
|
+
let text_content = collect_text(node);
|
|
402
|
+
write!(
|
|
403
|
+
context,
|
|
404
|
+
r##"<a href="#{id}" aria-label="Link to heading '"##
|
|
405
|
+
)?;
|
|
406
|
+
context.escape(&text_content)?;
|
|
407
|
+
context.write_str(r#"'" data-heading-content=""#)?;
|
|
408
|
+
context.escape(&text_content)?;
|
|
409
|
+
context.write_str(r#"" class="anchor"></a>"#)?;
|
|
410
|
+
}
|
|
411
|
+
writeln!(context, "</h{}>", nh.level)?;
|
|
412
|
+
}
|
|
344
413
|
Ok(ChildRendering::HTML)
|
|
345
414
|
}
|
|
346
415
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
use comrak::
|
|
1
|
+
use comrak::options::Plugins;
|
|
2
|
+
use comrak::{parse_document, Arena};
|
|
2
3
|
use serde::Deserialize;
|
|
3
4
|
|
|
4
5
|
use crate::formatter::{CustomFormatter, RenderUserData};
|
|
@@ -9,6 +10,8 @@ pub struct RenderOptions {
|
|
|
9
10
|
pub alerts: bool,
|
|
10
11
|
pub autolink: bool,
|
|
11
12
|
pub cjk_friendly_emphasis: bool,
|
|
13
|
+
/// Only use default comrak HTML formatting
|
|
14
|
+
pub default_html: bool,
|
|
12
15
|
// pub default_info_string: String,
|
|
13
16
|
pub description_lists: bool,
|
|
14
17
|
pub escape: bool,
|
|
@@ -22,16 +25,27 @@ pub struct RenderOptions {
|
|
|
22
25
|
pub github_pre_lang: bool,
|
|
23
26
|
pub greentext: bool,
|
|
24
27
|
pub hardbreaks: bool,
|
|
28
|
+
/// Use new header/anchor combination in HTML output, per
|
|
29
|
+
/// https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/merge_requests/112.
|
|
30
|
+
pub header_accessibility: bool,
|
|
25
31
|
pub header_ids: Option<String>,
|
|
26
32
|
pub ignore_empty_links: bool,
|
|
27
33
|
pub ignore_setext: bool,
|
|
34
|
+
/// Detect inapplicable tasks (`- [~]`)
|
|
35
|
+
pub inapplicable_tasks: bool,
|
|
28
36
|
pub math_code: bool,
|
|
29
37
|
pub math_dollars: bool,
|
|
30
38
|
pub multiline_block_quotes: bool,
|
|
39
|
+
/// When the 'escaped_char_spans' render option is enabled, only emit `<span
|
|
40
|
+
/// data-escaped-char>` around the given characters.
|
|
41
|
+
pub only_escape_chars: Option<Vec<char>>,
|
|
42
|
+
/// Detect and mark potential placeholder variables, which
|
|
43
|
+
/// have the format `%{PLACEHOLDER}`
|
|
44
|
+
pub placeholder_detection: bool,
|
|
31
45
|
pub relaxed_autolinks: bool,
|
|
32
46
|
pub relaxed_tasklist_character: bool,
|
|
33
|
-
pub sourcepos: bool,
|
|
34
47
|
pub smart: bool,
|
|
48
|
+
pub sourcepos: bool,
|
|
35
49
|
pub spoiler: bool,
|
|
36
50
|
pub strikethrough: bool,
|
|
37
51
|
pub subscript: bool,
|
|
@@ -41,40 +55,11 @@ pub struct RenderOptions {
|
|
|
41
55
|
pub tagfilter: bool,
|
|
42
56
|
pub tasklist: bool,
|
|
43
57
|
pub tasklist_classes: bool,
|
|
58
|
+
pub tasklist_in_table: bool,
|
|
44
59
|
pub underline: bool,
|
|
45
60
|
pub r#unsafe: bool,
|
|
46
61
|
pub wikilinks_title_after_pipe: bool,
|
|
47
62
|
pub wikilinks_title_before_pipe: bool,
|
|
48
|
-
|
|
49
|
-
/// GLFM specific options
|
|
50
|
-
|
|
51
|
-
/// Only use default comrak HTML formatting
|
|
52
|
-
pub default_html: bool,
|
|
53
|
-
|
|
54
|
-
/// Detect inapplicable tasks (`- [~]`)
|
|
55
|
-
pub inapplicable_tasks: bool,
|
|
56
|
-
|
|
57
|
-
/// Detect and mark potential placeholder variables, which
|
|
58
|
-
/// have the format `%{PLACEHOLDER}`
|
|
59
|
-
pub placeholder_detection: bool,
|
|
60
|
-
|
|
61
|
-
/// When the 'escaped_char_spans' render option is enabled, only emit `<span
|
|
62
|
-
/// data-escaped-char>` around the given characters.
|
|
63
|
-
pub only_escape_chars: Option<Vec<char>>,
|
|
64
|
-
|
|
65
|
-
pub debug: bool,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
impl From<&RenderOptions> for RenderUserData {
|
|
69
|
-
fn from(options: &RenderOptions) -> Self {
|
|
70
|
-
RenderUserData {
|
|
71
|
-
default_html: options.default_html,
|
|
72
|
-
inapplicable_tasks: options.inapplicable_tasks,
|
|
73
|
-
placeholder_detection: options.placeholder_detection,
|
|
74
|
-
only_escape_chars: options.only_escape_chars.clone(),
|
|
75
|
-
debug: options.debug,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
63
|
}
|
|
79
64
|
|
|
80
65
|
impl From<&RenderOptions> for comrak::Options<'_> {
|
|
@@ -112,24 +97,25 @@ impl From<&RenderOptions> for comrak::Options<'_> {
|
|
|
112
97
|
comrak_options.render.github_pre_lang = options.github_pre_lang;
|
|
113
98
|
comrak_options.render.hardbreaks = options.hardbreaks;
|
|
114
99
|
comrak_options.render.ignore_empty_links = options.ignore_empty_links;
|
|
115
|
-
comrak_options.render.ignore_setext = options.ignore_setext;
|
|
116
100
|
comrak_options.render.sourcepos = options.sourcepos;
|
|
117
101
|
comrak_options.render.tasklist_classes = options.tasklist_classes;
|
|
118
102
|
// comrak_options.render.syntax_highlighting = options.syntax_highlighting;
|
|
119
103
|
|
|
120
|
-
comrak_options.render.
|
|
104
|
+
comrak_options.render.r#unsafe = options.r#unsafe;
|
|
121
105
|
|
|
122
106
|
// comrak_options.parse.default_info_string = options.default_info_string;
|
|
107
|
+
comrak_options.parse.ignore_setext = options.ignore_setext;
|
|
123
108
|
comrak_options.parse.relaxed_autolinks = options.relaxed_autolinks;
|
|
124
109
|
comrak_options.parse.relaxed_tasklist_matching = options.relaxed_tasklist_character;
|
|
125
110
|
comrak_options.parse.smart = options.smart;
|
|
111
|
+
comrak_options.parse.tasklist_in_table = options.tasklist_in_table;
|
|
126
112
|
|
|
127
113
|
comrak_options
|
|
128
114
|
}
|
|
129
115
|
}
|
|
130
116
|
|
|
131
117
|
pub fn render(text: &str, options: &RenderOptions) -> String {
|
|
132
|
-
render_with_plugins(text, options, &
|
|
118
|
+
render_with_plugins(text, options, &Plugins::default())
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
fn render_with_plugins(text: &str, render_options: &RenderOptions, plugins: &Plugins) -> String {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
use comrak::{escape_commonmark_inline, escape_commonmark_link_destination};
|
|
2
|
-
use magnus::{
|
|
2
|
+
use magnus::{function, prelude::*, Error, RHash, RString, Ruby};
|
|
3
3
|
use serde_magnus::deserialize;
|
|
4
4
|
|
|
5
5
|
mod formatter;
|
|
6
6
|
mod glfm;
|
|
7
7
|
use glfm::{render, RenderOptions};
|
|
8
8
|
|
|
9
|
-
pub fn render_to_html_rs(text: RString, options: RHash) -> Result<String, Error> {
|
|
10
|
-
let render_options: RenderOptions = deserialize(options)?;
|
|
9
|
+
pub fn render_to_html_rs(ruby: &Ruby, text: RString, options: RHash) -> Result<String, Error> {
|
|
10
|
+
let render_options: RenderOptions = deserialize(ruby, options)?;
|
|
11
11
|
|
|
12
12
|
// SAFETY: `RString::as_str` returns a reference directly to Ruby memory.
|
|
13
13
|
// We do not hold onto or save the `&str`, or otherwise permit Ruby to GC or
|
|
@@ -35,8 +35,8 @@ pub fn escape_commonmark_link_destination_rs(text: RString) -> Result<String, Er
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
#[magnus::init]
|
|
38
|
-
fn init() -> Result<(), Error> {
|
|
39
|
-
let module = define_module("GLFMMarkdown")?;
|
|
38
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
39
|
+
let module = ruby.define_module("GLFMMarkdown")?;
|
|
40
40
|
|
|
41
41
|
module.define_singleton_method("render_to_html_rs", function!(render_to_html_rs, 2))?;
|
|
42
42
|
module.define_singleton_method(
|