gitlab-glfm-markdown 0.0.41 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gitlab-glfm-markdown might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/gitlab-glfm-markdown.rb +9 -61
- metadata +12 -73
- data/Cargo.lock +0 -966
- data/LICENSE +0 -28
- data/README.md +0 -120
- data/ext/gitlab_glfm_markdown/Cargo.lock +0 -1
- data/ext/gitlab_glfm_markdown/Cargo.toml +0 -27
- data/ext/gitlab_glfm_markdown/extconf.rb +0 -10
- data/ext/gitlab_glfm_markdown/src/formatter.rs +0 -546
- data/ext/gitlab_glfm_markdown/src/glfm.rs +0 -136
- data/ext/gitlab_glfm_markdown/src/lib.rs +0 -52
- data/ext/gitlab_glfm_markdown/src/main.rs +0 -272
- data/lib/gitlab_glfm_markdown/loader.rb +0 -8
- data/lib/gitlab_glfm_markdown/version.rb +0 -5
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
use std::fmt::{self, Write};
|
|
2
|
-
use std::sync::LazyLock;
|
|
3
|
-
|
|
4
|
-
use comrak::html::{collect_text, format_node_default, render_sourcepos, ChildRendering, Context};
|
|
5
|
-
use comrak::nodes::{
|
|
6
|
-
ListType, NodeHeading, NodeLink, NodeList, NodeTaskItem, NodeValue, TableAlignment,
|
|
7
|
-
};
|
|
8
|
-
use comrak::{create_formatter, html, node_matches, Node};
|
|
9
|
-
use regex::Regex;
|
|
10
|
-
|
|
11
|
-
use crate::glfm::RenderOptions;
|
|
12
|
-
|
|
13
|
-
static PLACEHOLDER_REGEX: LazyLock<Regex> =
|
|
14
|
-
LazyLock::new(|| Regex::new(r"%\{(?:\w{1,30})}").unwrap());
|
|
15
|
-
|
|
16
|
-
#[derive(Default)]
|
|
17
|
-
pub struct RenderUserData {
|
|
18
|
-
pub default_html: bool,
|
|
19
|
-
pub inapplicable_tasks: bool,
|
|
20
|
-
pub placeholder_detection: bool,
|
|
21
|
-
pub only_escape_chars: Option<Vec<char>>,
|
|
22
|
-
pub header_accessibility: bool,
|
|
23
|
-
|
|
24
|
-
last_heading: Option<String>,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
impl From<&RenderOptions> for RenderUserData {
|
|
28
|
-
fn from(options: &RenderOptions) -> Self {
|
|
29
|
-
RenderUserData {
|
|
30
|
-
default_html: options.default_html,
|
|
31
|
-
inapplicable_tasks: options.inapplicable_tasks,
|
|
32
|
-
placeholder_detection: options.placeholder_detection,
|
|
33
|
-
only_escape_chars: options.only_escape_chars.clone(),
|
|
34
|
-
header_accessibility: options.header_accessibility,
|
|
35
|
-
last_heading: None,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// The important thing to remember is that this overrides the default behavior of the
|
|
41
|
-
// specified nodes. If we do override a node, then it's our responsibility to ensure that
|
|
42
|
-
// any changes in the Comrak code for those nodes is backported to here, such as when
|
|
43
|
-
// `figcaption` support was added.
|
|
44
|
-
//
|
|
45
|
-
// One idea to limit that would be having the ability to specify attributes that would
|
|
46
|
-
// be inserted when a node is rendered. That would allow us to (in many cases) just
|
|
47
|
-
// inject the changes we need. Such a feature would need to be added to Comrak.
|
|
48
|
-
create_formatter!(CustomFormatter<RenderUserData>, {
|
|
49
|
-
NodeValue::Text(ref literal) => |context, node, entering| {
|
|
50
|
-
return render_text(context, node, entering, literal);
|
|
51
|
-
},
|
|
52
|
-
NodeValue::Link(ref nl) => |context, node, entering| {
|
|
53
|
-
return render_link(context, node, entering, nl);
|
|
54
|
-
},
|
|
55
|
-
NodeValue::Image(ref nl) => |context, node, entering| {
|
|
56
|
-
return render_image(context, node, entering, nl);
|
|
57
|
-
},
|
|
58
|
-
NodeValue::List(ref nl) => |context, node, entering| {
|
|
59
|
-
return render_list(context, node, entering, nl);
|
|
60
|
-
},
|
|
61
|
-
NodeValue::TaskItem(ref nti) => |context, node, entering| {
|
|
62
|
-
return render_task_item(context, node, entering, nti);
|
|
63
|
-
},
|
|
64
|
-
NodeValue::TableCell => |context, node, entering| {
|
|
65
|
-
return render_table_cell(context, node, entering);
|
|
66
|
-
},
|
|
67
|
-
NodeValue::Escaped => |context, node, entering| {
|
|
68
|
-
return render_escaped(context, node, entering);
|
|
69
|
-
},
|
|
70
|
-
NodeValue::Heading(ref nh) => |context, node, entering| {
|
|
71
|
-
return render_heading(context, node, entering, nh);
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
fn render_text(
|
|
76
|
-
context: &mut Context<RenderUserData>,
|
|
77
|
-
node: Node<'_>,
|
|
78
|
-
entering: bool,
|
|
79
|
-
literal: &str,
|
|
80
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
81
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
|
|
82
|
-
return html::format_node_default(context, node, entering);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if entering {
|
|
86
|
-
let mut cursor: usize = 0;
|
|
87
|
-
|
|
88
|
-
for mat in PLACEHOLDER_REGEX.find_iter(literal) {
|
|
89
|
-
if mat.start() > cursor {
|
|
90
|
-
context.escape(&literal[cursor..mat.start()])?;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
context.write_str("<span data-placeholder>")?;
|
|
94
|
-
context.escape(&literal[mat.start()..mat.end()])?;
|
|
95
|
-
context.write_str("</span>")?;
|
|
96
|
-
|
|
97
|
-
cursor = mat.end();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if cursor < literal.len() {
|
|
101
|
-
context.escape(&literal[cursor..])?;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
Ok(ChildRendering::HTML)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
fn render_link(
|
|
109
|
-
context: &mut Context<RenderUserData>,
|
|
110
|
-
node: Node<'_>,
|
|
111
|
-
entering: bool,
|
|
112
|
-
nl: &NodeLink,
|
|
113
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
114
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
115
|
-
return html::format_node_default(context, node, entering);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let parent_node = node.parent();
|
|
119
|
-
|
|
120
|
-
if !context.options.parse.relaxed_autolinks
|
|
121
|
-
|| (parent_node.is_none()
|
|
122
|
-
|| !matches!(
|
|
123
|
-
parent_node.unwrap().data.borrow().value,
|
|
124
|
-
NodeValue::Link(..)
|
|
125
|
-
))
|
|
126
|
-
{
|
|
127
|
-
if entering {
|
|
128
|
-
context.write_str("<a")?;
|
|
129
|
-
html::render_sourcepos(context, node)?;
|
|
130
|
-
context.write_str(" href=\"")?;
|
|
131
|
-
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
132
|
-
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
|
133
|
-
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
134
|
-
} else {
|
|
135
|
-
context.escape_href(&nl.url)?;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
context.write_str("\"")?;
|
|
139
|
-
|
|
140
|
-
if !nl.title.is_empty() {
|
|
141
|
-
context.write_str(" title=\"")?;
|
|
142
|
-
context.escape(&nl.title)?;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
146
|
-
context.write_str(" data-placeholder")?;
|
|
147
|
-
|
|
148
|
-
context.write_str(">")?;
|
|
149
|
-
} else {
|
|
150
|
-
context.write_str("</a>")?;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
Ok(ChildRendering::HTML)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fn render_image(
|
|
158
|
-
context: &mut Context<RenderUserData>,
|
|
159
|
-
node: Node<'_>,
|
|
160
|
-
entering: bool,
|
|
161
|
-
nl: &NodeLink,
|
|
162
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
163
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
164
|
-
return html::format_node_default(context, node, entering);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if entering {
|
|
168
|
-
if context.options.render.figure_with_caption {
|
|
169
|
-
context.write_str("<figure>")?;
|
|
170
|
-
}
|
|
171
|
-
context.write_str("<img")?;
|
|
172
|
-
html::render_sourcepos(context, node)?;
|
|
173
|
-
context.write_str(" src=\"")?;
|
|
174
|
-
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
175
|
-
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
|
|
176
|
-
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
177
|
-
} else {
|
|
178
|
-
context.escape_href(&nl.url)?;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
context.write_str("\"")?;
|
|
183
|
-
|
|
184
|
-
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
185
|
-
context.write_str(" data-placeholder")?;
|
|
186
|
-
|
|
187
|
-
context.write_str(" alt=\"")?;
|
|
188
|
-
|
|
189
|
-
return Ok(ChildRendering::Plain);
|
|
190
|
-
} else {
|
|
191
|
-
if !nl.title.is_empty() {
|
|
192
|
-
context.write_str("\" title=\"")?;
|
|
193
|
-
context.escape(&nl.title)?;
|
|
194
|
-
}
|
|
195
|
-
context.write_str("\" />")?;
|
|
196
|
-
if context.options.render.figure_with_caption {
|
|
197
|
-
if !nl.title.is_empty() {
|
|
198
|
-
context.write_str("<figcaption>")?;
|
|
199
|
-
context.escape(&nl.title)?;
|
|
200
|
-
context.write_str("</figcaption>")?;
|
|
201
|
-
}
|
|
202
|
-
context.write_str("</figure>")?;
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
Ok(ChildRendering::HTML)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Overridden to use class `task-list` instead of `contains-task-list`
|
|
210
|
-
// to align with GitLab class usage
|
|
211
|
-
fn render_list(
|
|
212
|
-
context: &mut Context<RenderUserData>,
|
|
213
|
-
node: Node<'_>,
|
|
214
|
-
entering: bool,
|
|
215
|
-
nl: &NodeList,
|
|
216
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
217
|
-
if !entering || !context.options.render.tasklist_classes {
|
|
218
|
-
return html::format_node_default(context, node, entering);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
context.cr()?;
|
|
222
|
-
match nl.list_type {
|
|
223
|
-
ListType::Bullet => {
|
|
224
|
-
context.write_str("<ul")?;
|
|
225
|
-
if nl.is_task_list {
|
|
226
|
-
context.write_str(" class=\"task-list\"")?;
|
|
227
|
-
}
|
|
228
|
-
html::render_sourcepos(context, node)?;
|
|
229
|
-
context.write_str(">\n")?;
|
|
230
|
-
}
|
|
231
|
-
ListType::Ordered => {
|
|
232
|
-
context.write_str("<ol")?;
|
|
233
|
-
if nl.is_task_list {
|
|
234
|
-
context.write_str(" class=\"task-list\"")?;
|
|
235
|
-
}
|
|
236
|
-
html::render_sourcepos(context, node)?;
|
|
237
|
-
if nl.start == 1 {
|
|
238
|
-
context.write_str(">\n")?;
|
|
239
|
-
} else {
|
|
240
|
-
writeln!(context, " start=\"{}\">", nl.start)?;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
Ok(ChildRendering::HTML)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Overridden to:
|
|
249
|
-
// 1. Detect inapplicable task list items; and,
|
|
250
|
-
// 2. Output checkbox sourcepos.
|
|
251
|
-
fn render_task_item(
|
|
252
|
-
context: &mut Context<RenderUserData>,
|
|
253
|
-
node: Node<'_>,
|
|
254
|
-
entering: bool,
|
|
255
|
-
nti: &NodeTaskItem,
|
|
256
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
257
|
-
let write_li = node
|
|
258
|
-
.parent()
|
|
259
|
-
.map(|p| node_matches!(p, NodeValue::List(_)))
|
|
260
|
-
.unwrap_or_default();
|
|
261
|
-
|
|
262
|
-
if !entering {
|
|
263
|
-
if write_li {
|
|
264
|
-
context.write_str("</li>\n")?;
|
|
265
|
-
}
|
|
266
|
-
return Ok(ChildRendering::HTML);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
match nti.symbol {
|
|
270
|
-
Some('~') => {
|
|
271
|
-
render_task_item_with(
|
|
272
|
-
context,
|
|
273
|
-
node,
|
|
274
|
-
nti,
|
|
275
|
-
if context.user.inapplicable_tasks {
|
|
276
|
-
TaskItemState::Inapplicable
|
|
277
|
-
} else {
|
|
278
|
-
TaskItemState::Checked
|
|
279
|
-
},
|
|
280
|
-
write_li,
|
|
281
|
-
)?;
|
|
282
|
-
}
|
|
283
|
-
None => {
|
|
284
|
-
render_task_item_with(context, node, nti, TaskItemState::Unchecked, write_li)?;
|
|
285
|
-
}
|
|
286
|
-
Some(ws) if ws.is_whitespace() => {
|
|
287
|
-
render_task_item_with(context, node, nti, TaskItemState::Unchecked, write_li)?;
|
|
288
|
-
}
|
|
289
|
-
Some('x' | 'X') => {
|
|
290
|
-
render_task_item_with(context, node, nti, TaskItemState::Checked, write_li)?;
|
|
291
|
-
}
|
|
292
|
-
Some(symbol) => {
|
|
293
|
-
context.cr()?;
|
|
294
|
-
if write_li {
|
|
295
|
-
context.write_str("<li")?;
|
|
296
|
-
if context.options.render.tasklist_classes {
|
|
297
|
-
context.write_str(" class=\"task-list-item\"")?;
|
|
298
|
-
}
|
|
299
|
-
html::render_sourcepos(context, node)?;
|
|
300
|
-
context.write_str(">")?;
|
|
301
|
-
}
|
|
302
|
-
context.write_str("[")?;
|
|
303
|
-
context.escape(&symbol.to_string())?;
|
|
304
|
-
context.write_str("] ")?;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
Ok(ChildRendering::HTML)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
#[derive(PartialEq)]
|
|
312
|
-
enum TaskItemState {
|
|
313
|
-
Checked,
|
|
314
|
-
Unchecked,
|
|
315
|
-
Inapplicable,
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
fn render_task_item_with(
|
|
319
|
-
context: &mut Context<RenderUserData>,
|
|
320
|
-
node: Node<'_>,
|
|
321
|
-
nti: &NodeTaskItem,
|
|
322
|
-
state: TaskItemState,
|
|
323
|
-
write_li: bool,
|
|
324
|
-
) -> fmt::Result {
|
|
325
|
-
context.cr()?;
|
|
326
|
-
if write_li {
|
|
327
|
-
context.write_str("<li")?;
|
|
328
|
-
if context.options.render.tasklist_classes {
|
|
329
|
-
context.write_str(" class=\"")?;
|
|
330
|
-
if state == TaskItemState::Inapplicable {
|
|
331
|
-
context.write_str("inapplicable ")?;
|
|
332
|
-
}
|
|
333
|
-
context.write_str("task-list-item\"")?;
|
|
334
|
-
}
|
|
335
|
-
render_sourcepos(context, node)?;
|
|
336
|
-
context.write_str(">")?;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
context.write_str("<input type=\"checkbox\"")?;
|
|
340
|
-
if !write_li {
|
|
341
|
-
html::render_sourcepos(context, node)?;
|
|
342
|
-
}
|
|
343
|
-
if context.options.render.sourcepos {
|
|
344
|
-
write!(
|
|
345
|
-
context,
|
|
346
|
-
" data-checkbox-sourcepos=\"{:?}\"",
|
|
347
|
-
nti.symbol_sourcepos
|
|
348
|
-
)?;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if context.options.render.tasklist_classes {
|
|
352
|
-
context.write_str(" class=\"task-list-item-checkbox\"")?;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
match state {
|
|
356
|
-
TaskItemState::Checked => {
|
|
357
|
-
context.write_str(" checked=\"\"")?;
|
|
358
|
-
}
|
|
359
|
-
TaskItemState::Unchecked => {}
|
|
360
|
-
TaskItemState::Inapplicable => {
|
|
361
|
-
context.write_str(" data-inapplicable")?;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
context.write_str(" disabled=\"\" /> ")?;
|
|
365
|
-
|
|
366
|
-
Ok(())
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
fn render_table_cell<T>(
|
|
370
|
-
context: &mut Context<T>,
|
|
371
|
-
node: Node<'_>,
|
|
372
|
-
entering: bool,
|
|
373
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
374
|
-
let Some(row_node) = node.parent() else {
|
|
375
|
-
panic!("rendered a table cell without a containing table row");
|
|
376
|
-
};
|
|
377
|
-
let row = &row_node.data().value;
|
|
378
|
-
let in_header = match *row {
|
|
379
|
-
NodeValue::TableRow(header) => header,
|
|
380
|
-
_ => panic!("rendered a table cell contained by something other than a table row"),
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
let Some(table_node) = row_node.parent() else {
|
|
384
|
-
panic!("rendered a table cell without a containing table");
|
|
385
|
-
};
|
|
386
|
-
let table = &table_node.data().value;
|
|
387
|
-
let alignments = match table {
|
|
388
|
-
NodeValue::Table(nt) => &nt.alignments,
|
|
389
|
-
_ => {
|
|
390
|
-
panic!("rendered a table cell in a table row contained by something other than a table")
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
if entering {
|
|
395
|
-
context.cr()?;
|
|
396
|
-
if in_header {
|
|
397
|
-
context.write_str("<th")?;
|
|
398
|
-
render_sourcepos(context, node)?;
|
|
399
|
-
} else {
|
|
400
|
-
context.write_str("<td")?;
|
|
401
|
-
render_sourcepos(context, node)?;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
let mut start = row_node.first_child().unwrap(); // guaranteed to exist because `node' itself does!
|
|
405
|
-
let mut i = 0;
|
|
406
|
-
while !start.same_node(node) {
|
|
407
|
-
i += 1;
|
|
408
|
-
start = start.next_sibling().unwrap();
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
match alignments[i] {
|
|
412
|
-
TableAlignment::Left => {
|
|
413
|
-
context.write_str(" align=\"left\"")?;
|
|
414
|
-
}
|
|
415
|
-
TableAlignment::Right => {
|
|
416
|
-
context.write_str(" align=\"right\"")?;
|
|
417
|
-
}
|
|
418
|
-
TableAlignment::Center => {
|
|
419
|
-
context.write_str(" align=\"center\"")?;
|
|
420
|
-
}
|
|
421
|
-
TableAlignment::None => (),
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if context.options.parse.tasklist_in_table
|
|
425
|
-
&& context.options.render.tasklist_classes
|
|
426
|
-
&& contains_only_task_item(node)
|
|
427
|
-
{
|
|
428
|
-
context.write_str(" class=\"task-table-item\"")?;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
context.write_str(">")?;
|
|
432
|
-
} else if in_header {
|
|
433
|
-
context.write_str("</th>")?;
|
|
434
|
-
} else {
|
|
435
|
-
context.write_str("</td>")?;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
Ok(ChildRendering::HTML)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
fn render_escaped(
|
|
442
|
-
context: &mut Context<RenderUserData>,
|
|
443
|
-
node: Node<'_>,
|
|
444
|
-
entering: bool,
|
|
445
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
446
|
-
if !context.options.render.escaped_char_spans {
|
|
447
|
-
return Ok(ChildRendering::HTML);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if context
|
|
451
|
-
.user
|
|
452
|
-
.only_escape_chars
|
|
453
|
-
.as_ref()
|
|
454
|
-
.is_none_or(|only_escape_chars| {
|
|
455
|
-
with_node_text_content(node, false, |content| {
|
|
456
|
-
content.chars().count() == 1
|
|
457
|
-
&& only_escape_chars.contains(&content.chars().next().unwrap())
|
|
458
|
-
})
|
|
459
|
-
})
|
|
460
|
-
{
|
|
461
|
-
if entering {
|
|
462
|
-
context.write_str("<span data-escaped-char")?;
|
|
463
|
-
render_sourcepos(context, node)?;
|
|
464
|
-
context.write_str(">")?;
|
|
465
|
-
} else {
|
|
466
|
-
context.write_str("</span>")?;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
Ok(ChildRendering::HTML)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
fn render_heading(
|
|
474
|
-
context: &mut Context<RenderUserData>,
|
|
475
|
-
node: Node<'_>,
|
|
476
|
-
entering: bool,
|
|
477
|
-
nh: &NodeHeading,
|
|
478
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
479
|
-
if !context.user.header_accessibility || context.plugins.render.heading_adapter.is_some() {
|
|
480
|
-
return format_node_default(context, node, entering);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if entering {
|
|
484
|
-
context.cr()?;
|
|
485
|
-
write!(context, "<h{}", nh.level)?;
|
|
486
|
-
|
|
487
|
-
if let Some(ref prefix) = context.options.extension.header_ids {
|
|
488
|
-
let text_content = collect_text(node);
|
|
489
|
-
let id = context.anchorizer.anchorize(&text_content);
|
|
490
|
-
write!(context, r##" id="{prefix}{id}""##)?;
|
|
491
|
-
context.user.last_heading = Some(id);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
render_sourcepos(context, node)?;
|
|
495
|
-
context.write_str(">")?;
|
|
496
|
-
} else {
|
|
497
|
-
if context.options.extension.header_ids.is_some() {
|
|
498
|
-
let id = context.user.last_heading.take().unwrap();
|
|
499
|
-
let text_content = collect_text(node);
|
|
500
|
-
write!(
|
|
501
|
-
context,
|
|
502
|
-
r##"<a href="#{id}" aria-label="Link to heading '"##
|
|
503
|
-
)?;
|
|
504
|
-
context.escape(&text_content)?;
|
|
505
|
-
context.write_str(r#"'" data-heading-content=""#)?;
|
|
506
|
-
context.escape(&text_content)?;
|
|
507
|
-
context.write_str(r#"" class="anchor"></a>"#)?;
|
|
508
|
-
}
|
|
509
|
-
writeln!(context, "</h{}>", nh.level)?;
|
|
510
|
-
}
|
|
511
|
-
Ok(ChildRendering::HTML)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/// If the given node has a single text child, apply a function to the text content
|
|
515
|
-
/// of that node. Otherwise, return the given default value.
|
|
516
|
-
fn with_node_text_content<U, F>(node: Node<'_>, default: U, f: F) -> U
|
|
517
|
-
where
|
|
518
|
-
F: FnOnce(&str) -> U,
|
|
519
|
-
{
|
|
520
|
-
let Some(child) = node.first_child() else {
|
|
521
|
-
return default;
|
|
522
|
-
};
|
|
523
|
-
let Some(last_child) = node.last_child() else {
|
|
524
|
-
return default;
|
|
525
|
-
};
|
|
526
|
-
if !child.same_node(last_child) {
|
|
527
|
-
return default;
|
|
528
|
-
}
|
|
529
|
-
let NodeValue::Text(ref text) = child.data.borrow().value else {
|
|
530
|
-
return default;
|
|
531
|
-
};
|
|
532
|
-
f(text)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
fn contains_only_task_item(node: Node<'_>) -> bool {
|
|
536
|
-
let Some(first) = node.first_child() else {
|
|
537
|
-
return false;
|
|
538
|
-
};
|
|
539
|
-
if !matches!(first.data().value, NodeValue::TaskItem(..)) {
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
if first.next_sibling().is_some() {
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
545
|
-
true
|
|
546
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
use comrak::options::Plugins;
|
|
2
|
-
use comrak::{parse_document, Arena};
|
|
3
|
-
use serde::Deserialize;
|
|
4
|
-
|
|
5
|
-
use crate::formatter::{CustomFormatter, RenderUserData};
|
|
6
|
-
|
|
7
|
-
#[derive(Debug, Default, Deserialize)]
|
|
8
|
-
#[serde(default)]
|
|
9
|
-
pub struct RenderOptions {
|
|
10
|
-
pub alerts: bool,
|
|
11
|
-
pub autolink: bool,
|
|
12
|
-
pub cjk_friendly_emphasis: bool,
|
|
13
|
-
/// Only use default comrak HTML formatting
|
|
14
|
-
pub default_html: bool,
|
|
15
|
-
// pub default_info_string: String,
|
|
16
|
-
pub description_lists: bool,
|
|
17
|
-
pub escape: bool,
|
|
18
|
-
pub escaped_char_spans: bool,
|
|
19
|
-
pub figure_with_caption: bool,
|
|
20
|
-
pub footnotes: bool,
|
|
21
|
-
// pub front_matter_delimiter: String,
|
|
22
|
-
pub full_info_string: bool,
|
|
23
|
-
pub gemojis: bool,
|
|
24
|
-
pub gfm_quirks: bool,
|
|
25
|
-
pub github_pre_lang: bool,
|
|
26
|
-
pub greentext: bool,
|
|
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,
|
|
31
|
-
pub header_ids: Option<String>,
|
|
32
|
-
pub ignore_empty_links: bool,
|
|
33
|
-
pub ignore_setext: bool,
|
|
34
|
-
/// Detect inapplicable tasks (`- [~]`)
|
|
35
|
-
pub inapplicable_tasks: bool,
|
|
36
|
-
pub math_code: bool,
|
|
37
|
-
pub math_dollars: bool,
|
|
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,
|
|
45
|
-
pub relaxed_autolinks: bool,
|
|
46
|
-
pub relaxed_tasklist_character: bool,
|
|
47
|
-
pub smart: bool,
|
|
48
|
-
pub sourcepos: bool,
|
|
49
|
-
pub spoiler: bool,
|
|
50
|
-
pub strikethrough: bool,
|
|
51
|
-
pub subscript: bool,
|
|
52
|
-
pub superscript: bool,
|
|
53
|
-
// pub syntax_highlighting: String,
|
|
54
|
-
pub table: bool,
|
|
55
|
-
pub tagfilter: bool,
|
|
56
|
-
pub tasklist: bool,
|
|
57
|
-
pub tasklist_classes: bool,
|
|
58
|
-
pub tasklist_in_table: bool,
|
|
59
|
-
pub underline: bool,
|
|
60
|
-
pub r#unsafe: bool,
|
|
61
|
-
pub wikilinks_title_after_pipe: bool,
|
|
62
|
-
pub wikilinks_title_before_pipe: bool,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
impl From<&RenderOptions> for comrak::Options<'_> {
|
|
66
|
-
fn from(options: &RenderOptions) -> Self {
|
|
67
|
-
let mut comrak_options = comrak::Options::default();
|
|
68
|
-
|
|
69
|
-
comrak_options.extension.alerts = options.alerts;
|
|
70
|
-
comrak_options.extension.autolink = options.autolink;
|
|
71
|
-
comrak_options.extension.cjk_friendly_emphasis = options.cjk_friendly_emphasis;
|
|
72
|
-
comrak_options.extension.description_lists = options.description_lists;
|
|
73
|
-
comrak_options.extension.footnotes = options.footnotes;
|
|
74
|
-
// comrak_options.extension.front_matter_delimiter = options.front_matter_delimiter;
|
|
75
|
-
comrak_options.extension.greentext = options.greentext;
|
|
76
|
-
comrak_options.extension.header_ids = options.header_ids.clone();
|
|
77
|
-
comrak_options.extension.math_code = options.math_code;
|
|
78
|
-
comrak_options.extension.math_dollars = options.math_dollars;
|
|
79
|
-
comrak_options.extension.multiline_block_quotes = options.multiline_block_quotes;
|
|
80
|
-
comrak_options.extension.shortcodes = options.gemojis;
|
|
81
|
-
comrak_options.extension.spoiler = options.spoiler;
|
|
82
|
-
comrak_options.extension.strikethrough = options.strikethrough;
|
|
83
|
-
comrak_options.extension.subscript = options.subscript;
|
|
84
|
-
comrak_options.extension.superscript = options.superscript;
|
|
85
|
-
comrak_options.extension.table = options.table;
|
|
86
|
-
comrak_options.extension.tagfilter = options.tagfilter;
|
|
87
|
-
comrak_options.extension.tasklist = options.tasklist;
|
|
88
|
-
comrak_options.extension.underline = options.underline;
|
|
89
|
-
comrak_options.extension.wikilinks_title_after_pipe = options.wikilinks_title_after_pipe;
|
|
90
|
-
comrak_options.extension.wikilinks_title_before_pipe = options.wikilinks_title_before_pipe;
|
|
91
|
-
|
|
92
|
-
comrak_options.render.escape = options.escape;
|
|
93
|
-
comrak_options.render.escaped_char_spans = options.escaped_char_spans;
|
|
94
|
-
comrak_options.render.figure_with_caption = options.figure_with_caption;
|
|
95
|
-
comrak_options.render.full_info_string = options.full_info_string;
|
|
96
|
-
comrak_options.render.gfm_quirks = options.gfm_quirks;
|
|
97
|
-
comrak_options.render.github_pre_lang = options.github_pre_lang;
|
|
98
|
-
comrak_options.render.hardbreaks = options.hardbreaks;
|
|
99
|
-
comrak_options.render.ignore_empty_links = options.ignore_empty_links;
|
|
100
|
-
comrak_options.render.sourcepos = options.sourcepos;
|
|
101
|
-
comrak_options.render.tasklist_classes = options.tasklist_classes;
|
|
102
|
-
// comrak_options.render.syntax_highlighting = options.syntax_highlighting;
|
|
103
|
-
|
|
104
|
-
comrak_options.render.r#unsafe = options.r#unsafe;
|
|
105
|
-
|
|
106
|
-
// comrak_options.parse.default_info_string = options.default_info_string;
|
|
107
|
-
comrak_options.parse.ignore_setext = options.ignore_setext;
|
|
108
|
-
comrak_options.parse.relaxed_autolinks = options.relaxed_autolinks;
|
|
109
|
-
comrak_options.parse.relaxed_tasklist_matching = options.relaxed_tasklist_character;
|
|
110
|
-
comrak_options.parse.smart = options.smart;
|
|
111
|
-
comrak_options.parse.tasklist_in_table = options.tasklist_in_table;
|
|
112
|
-
|
|
113
|
-
comrak_options
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
pub fn render(text: &str, options: &RenderOptions) -> String {
|
|
118
|
-
render_with_plugins(text, options, &Plugins::default())
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
fn render_with_plugins(text: &str, render_options: &RenderOptions, plugins: &Plugins) -> String {
|
|
122
|
-
let user_data = RenderUserData::from(render_options);
|
|
123
|
-
let options = comrak::Options::from(render_options);
|
|
124
|
-
|
|
125
|
-
if user_data.default_html {
|
|
126
|
-
return comrak::markdown_to_html_with_plugins(text, &options, plugins);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let arena = Arena::new();
|
|
130
|
-
let root = parse_document(&arena, text, &options);
|
|
131
|
-
|
|
132
|
-
let mut out = String::new();
|
|
133
|
-
CustomFormatter::format_document_with_plugins(root, &options, &mut out, plugins, user_data)
|
|
134
|
-
.unwrap();
|
|
135
|
-
out
|
|
136
|
-
}
|