gitlab-glfm-markdown 0.0.38 → 0.0.40
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 +5 -5
- data/README.md +11 -9
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/Cargo.toml +4 -4
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/extconf.rb +1 -1
- data/ext/gitlab_glfm_markdown/src/formatter.rs +456 -0
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/glfm.rs +17 -32
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/main.rs +44 -41
- data/lib/{glfm_markdown.rb → gitlab-glfm-markdown.rb} +3 -5
- data/lib/{glfm_markdown → gitlab_glfm_markdown}/loader.rb +2 -2
- data/lib/{glfm_markdown → gitlab_glfm_markdown}/version.rb +1 -1
- metadata +13 -13
- data/ext/glfm_markdown/src/formatter.rs +0 -366
- /data/ext/{glfm_markdown → gitlab_glfm_markdown}/Cargo.lock +0 -0
- /data/ext/{glfm_markdown → gitlab_glfm_markdown}/src/lib.rs +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 958f895a64867a4cf0facd03a03f363398263613493019627df9ed9481dc0d74
|
|
4
|
+
data.tar.gz: c180c4b058f5c823b2dc00ad32bf63de261a070cba034326d0f29e1131ea6f53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1887cab2213baf059cf86655333308a4cd021bed71438a16c32f6a234dde93c29cc0b87522bc6057d6f87acf4150634c712f98ce273032845a90899c6a4806f
|
|
7
|
+
data.tar.gz: e7c2789aa86e38930c55d9d31520d947bee79cb093338213259d0820b8015abe1d3419ba1ce282af35495ca73fa66846745f983fec574921d97b301f725d9b7a
|
data/Cargo.lock
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This file is automatically @generated by Cargo.
|
|
2
2
|
# It is not intended for manual editing.
|
|
3
|
-
version =
|
|
3
|
+
version = 4
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "adler2"
|
|
@@ -221,9 +221,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
|
|
221
221
|
|
|
222
222
|
[[package]]
|
|
223
223
|
name = "comrak"
|
|
224
|
-
version = "0.
|
|
224
|
+
version = "0.49.0"
|
|
225
225
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
226
|
-
checksum = "
|
|
226
|
+
checksum = "ab87129dce2f2d7e75e753b1df0e5093b27dec8fa5970b6eb51280faacb25bd6"
|
|
227
227
|
dependencies = [
|
|
228
228
|
"caseless",
|
|
229
229
|
"emojis",
|
|
@@ -306,8 +306,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
306
306
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
|
-
name = "
|
|
310
|
-
version = "0.0.
|
|
309
|
+
name = "gitlab-glfm-markdown"
|
|
310
|
+
version = "0.0.40"
|
|
311
311
|
dependencies = [
|
|
312
312
|
"clap",
|
|
313
313
|
"comrak",
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/releases)
|
|
5
5
|
|
|
6
6
|
Implements GLFM (as used by GitLab) using the Rust-based Markdown parser
|
|
7
|
-
[Comrak](https://github.com/kivikakk/comrak) (0.
|
|
7
|
+
[Comrak](https://github.com/kivikakk/comrak) (0.49.0), providing a Ruby interface.
|
|
8
8
|
|
|
9
9
|
This project is still in constant flux, so interfaces and functionality can change at any time.
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ Try on command line:
|
|
|
26
26
|
rake compile
|
|
27
27
|
bin/console
|
|
28
28
|
|
|
29
|
-
require '
|
|
29
|
+
require 'gitlab-glfm-markdown'
|
|
30
30
|
|
|
31
31
|
GLFMMarkdown.to_html('# header', options: { sourcepos: true })
|
|
32
32
|
```
|
|
@@ -37,9 +37,10 @@ GLFMMarkdown.to_html('# header', options: { sourcepos: true })
|
|
|
37
37
|
|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
38
38
|
| `autolink` | Enable the `autolink` extension |
|
|
39
39
|
| `cjk_friendly_emphasis` | Enable the [`cjk_friendly_emphasis` extension](https://github.com/tats-u/markdown-cjk-friendly) |
|
|
40
|
+
| `default_html` | Disables any custom HTML, and returns default HTML from `comrak` |
|
|
40
41
|
| `description_lists` | Enable the `description-lists` extension |
|
|
41
|
-
| `escape` | Escape raw HTML instead of clobbering it |
|
|
42
42
|
| `escape_char_spans` | Wrap escaped characters in a `<span>` to allow any post-processing to recognize them |
|
|
43
|
+
| `escape` | Escape raw HTML instead of clobbering it |
|
|
43
44
|
| `figure_with_caption` | Render the image as a figure element with the title as its caption |
|
|
44
45
|
| `footnotes` | Enable the `footnotes` extension |
|
|
45
46
|
| `full_info_string` | Enable full info strings for code blocks |
|
|
@@ -48,6 +49,7 @@ GLFMMarkdown.to_html('# header', options: { sourcepos: true })
|
|
|
48
49
|
| `github_pre_lang` | Use GitHub-style `<pre lang>` for code blocks |
|
|
49
50
|
| `greentext` | Enable the `greentext` extension - requires at least one space after a `>` character to generate a blockquote, and restarts blockquote nesting across unique lines of input |
|
|
50
51
|
| `hardbreaks` | Treat newlines as hard line breaks |
|
|
52
|
+
| `header_accessibility` | Use new header/anchor combination in HTML output, per [!112](https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/merge_requests/112). |
|
|
51
53
|
| `header_ids <PREFIX>` | Enable the `header-id` extension, with the given ID prefix |
|
|
52
54
|
| `ignore_empty_links` | Ignore empty links in input |
|
|
53
55
|
| `ignore_setext` | Ignore setext headings in input |
|
|
@@ -55,24 +57,24 @@ GLFMMarkdown.to_html('# header', options: { sourcepos: true })
|
|
|
55
57
|
| `math_code` | Enables `math code` extension, using math code syntax |
|
|
56
58
|
| `math_dollars` | Enables `math dollars` extension, using math dollar syntax |
|
|
57
59
|
| `multiline_block_quotes` | Enable the `multiline-block-quotes` extension |
|
|
58
|
-
| `
|
|
60
|
+
| `only_escape_chars <CHARS>` | When the 'escaped_char_spans' render option is enabled, only emit `<span data-escaped-char>` around the given characters. |
|
|
59
61
|
| `placeholder_detection` | Detect placeholder variables in the format `%{placeholder}` |
|
|
60
62
|
| `relaxed_autolinks` | Enable relaxing of autolink parsing, allowing links to be recognized when in brackets, with any scheme, and with dot-less hostnames |
|
|
61
63
|
| `relaxed_tasklist_character` | Enable relaxing which character is allowed in tasklists |
|
|
62
|
-
| `sourcepos` | Include source mappings in HTML attributes |
|
|
63
64
|
| `smart` | Use smart punctuation |
|
|
65
|
+
| `sourcepos` | Include source mappings in HTML attributes |
|
|
64
66
|
| `spoiler` | Enable the `spoiler` extension - use double vertical bars |
|
|
65
67
|
| `strikethrough` | Enable the `strikethrough` extension |
|
|
66
68
|
| `superscript` | Enable the `superscript` extension |
|
|
67
69
|
| `table` | Enable the `table` extension |
|
|
68
70
|
| `tagfilter` | Enable the `tagfilter` extension |
|
|
69
|
-
| `tasklist` | Enable the `tasklist` extension |
|
|
70
71
|
| `tasklist_classes` | Output classes on tasklist elements so that they can be styled with CSS |
|
|
72
|
+
| `tasklist_in_table` | Enables parsing tasklist items within tables when they're the only content of a table cell |
|
|
73
|
+
| `tasklist` | Enable the `tasklist` extension |
|
|
71
74
|
| `underline` | Enables the `underline` extension - use double underscores |
|
|
72
75
|
| `unsafe` | Allow raw HTML and dangerous URLs |
|
|
73
76
|
| `wikilinks_title_after_pipe` | Enable the `wikilinks_title_after_pipe` extension |
|
|
74
77
|
| `wikilinks_title_before_pipe` | Enable the `wikilinks_title_before_pipe` extension |
|
|
75
|
-
| `debug` | Show debug information |
|
|
76
78
|
|
|
77
79
|
## Dingus / Demo
|
|
78
80
|
|
|
@@ -85,8 +87,8 @@ https://gitlab-org.gitlab.io/ruby/gems/gitlab-glfm-markdown
|
|
|
85
87
|
A command line executable can be built for debugging.
|
|
86
88
|
|
|
87
89
|
```
|
|
88
|
-
cargo run --bin
|
|
89
|
-
cargo run --bin
|
|
90
|
+
cargo run --bin gitlab-glfm-markdown --features="cli" -- --help
|
|
91
|
+
cargo run --bin gitlab-glfm-markdown --features="cli" -- --sourcepos
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
There is a VSCode workspace that allows you to `Debug executable`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
|
-
name = "
|
|
3
|
-
version = "0.0.
|
|
2
|
+
name = "gitlab-glfm-markdown"
|
|
3
|
+
version = "0.0.40"
|
|
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."
|
|
@@ -10,12 +10,12 @@ publish = false
|
|
|
10
10
|
crate-type = ["cdylib"]
|
|
11
11
|
|
|
12
12
|
[[bin]]
|
|
13
|
-
name = "
|
|
13
|
+
name = "gitlab-glfm-markdown"
|
|
14
14
|
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.49.0", default-features = false, features = ["shortcodes"] }
|
|
19
19
|
magnus = "0.8.2"
|
|
20
20
|
rb-sys = { version = "0.9.117", default-features = false, features = ["stable-api-compiled-fallback"] }
|
|
21
21
|
regex = "1.11.1"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'mkmf'
|
|
4
4
|
require 'rb_sys/mkmf'
|
|
5
5
|
|
|
6
|
-
create_rust_makefile('
|
|
6
|
+
create_rust_makefile('gitlab_glfm_markdown/gitlab_glfm_markdown') do |r|
|
|
7
7
|
r.auto_install_rust_toolchain = false
|
|
8
8
|
# Ensure all Rust dependencies are pinned when building
|
|
9
9
|
r.extra_cargo_args = ["--locked"]
|
|
@@ -0,0 +1,456 @@
|
|
|
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::{ListType, NodeHeading, NodeLink, NodeList, NodeTaskItem, NodeValue};
|
|
6
|
+
use comrak::{create_formatter, html, node_matches, Node};
|
|
7
|
+
use regex::Regex;
|
|
8
|
+
|
|
9
|
+
use crate::glfm::RenderOptions;
|
|
10
|
+
|
|
11
|
+
static PLACEHOLDER_REGEX: LazyLock<Regex> =
|
|
12
|
+
LazyLock::new(|| Regex::new(r"%\{(?:\w{1,30})}").unwrap());
|
|
13
|
+
|
|
14
|
+
#[derive(Default)]
|
|
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 header_accessibility: bool,
|
|
21
|
+
|
|
22
|
+
last_heading: Option<String>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl From<&RenderOptions> for RenderUserData {
|
|
26
|
+
fn from(options: &RenderOptions) -> Self {
|
|
27
|
+
RenderUserData {
|
|
28
|
+
default_html: options.default_html,
|
|
29
|
+
inapplicable_tasks: options.inapplicable_tasks,
|
|
30
|
+
placeholder_detection: options.placeholder_detection,
|
|
31
|
+
only_escape_chars: options.only_escape_chars.clone(),
|
|
32
|
+
header_accessibility: options.header_accessibility,
|
|
33
|
+
last_heading: None,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The important thing to remember is that this overrides the default behavior of the
|
|
39
|
+
// specified nodes. If we do override a node, then it's our responsibility to ensure that
|
|
40
|
+
// any changes in the Comrak code for those nodes is backported to here, such as when
|
|
41
|
+
// `figcaption` support was added.
|
|
42
|
+
//
|
|
43
|
+
// One idea to limit that would be having the ability to specify attributes that would
|
|
44
|
+
// be inserted when a node is rendered. That would allow us to (in many cases) just
|
|
45
|
+
// inject the changes we need. Such a feature would need to be added to Comrak.
|
|
46
|
+
create_formatter!(CustomFormatter<RenderUserData>, {
|
|
47
|
+
NodeValue::Text(ref literal) => |context, node, entering| {
|
|
48
|
+
return render_text(context, node, entering, literal);
|
|
49
|
+
},
|
|
50
|
+
NodeValue::Link(ref nl) => |context, node, entering| {
|
|
51
|
+
return render_link(context, node, entering, nl);
|
|
52
|
+
},
|
|
53
|
+
NodeValue::Image(ref nl) => |context, node, entering| {
|
|
54
|
+
return render_image(context, node, entering, nl);
|
|
55
|
+
},
|
|
56
|
+
NodeValue::List(ref nl) => |context, node, entering| {
|
|
57
|
+
return render_list(context, node, entering, nl);
|
|
58
|
+
},
|
|
59
|
+
NodeValue::TaskItem(ref nti) => |context, node, entering| {
|
|
60
|
+
return render_task_item(context, node, entering, nti);
|
|
61
|
+
},
|
|
62
|
+
NodeValue::Escaped => |context, node, entering| {
|
|
63
|
+
return render_escaped(context, node, entering);
|
|
64
|
+
},
|
|
65
|
+
NodeValue::Heading(ref nh) => |context, node, entering| {
|
|
66
|
+
return render_heading(context, node, entering, nh);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
fn render_text(
|
|
71
|
+
context: &mut Context<RenderUserData>,
|
|
72
|
+
node: Node<'_>,
|
|
73
|
+
entering: bool,
|
|
74
|
+
literal: &str,
|
|
75
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
76
|
+
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
|
|
77
|
+
return html::format_node_default(context, node, entering);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if entering {
|
|
81
|
+
let mut cursor: usize = 0;
|
|
82
|
+
|
|
83
|
+
for mat in PLACEHOLDER_REGEX.find_iter(literal) {
|
|
84
|
+
if mat.start() > cursor {
|
|
85
|
+
context.escape(&literal[cursor..mat.start()])?;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
context.write_str("<span data-placeholder>")?;
|
|
89
|
+
context.escape(&literal[mat.start()..mat.end()])?;
|
|
90
|
+
context.write_str("</span>")?;
|
|
91
|
+
|
|
92
|
+
cursor = mat.end();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if cursor < literal.len() {
|
|
96
|
+
context.escape(&literal[cursor..])?;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Ok(ChildRendering::HTML)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn render_link(
|
|
104
|
+
context: &mut Context<RenderUserData>,
|
|
105
|
+
node: Node<'_>,
|
|
106
|
+
entering: bool,
|
|
107
|
+
nl: &NodeLink,
|
|
108
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
109
|
+
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
110
|
+
return html::format_node_default(context, node, entering);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let parent_node = node.parent();
|
|
114
|
+
|
|
115
|
+
if !context.options.parse.relaxed_autolinks
|
|
116
|
+
|| (parent_node.is_none()
|
|
117
|
+
|| !matches!(
|
|
118
|
+
parent_node.unwrap().data.borrow().value,
|
|
119
|
+
NodeValue::Link(..)
|
|
120
|
+
))
|
|
121
|
+
{
|
|
122
|
+
if entering {
|
|
123
|
+
context.write_str("<a")?;
|
|
124
|
+
html::render_sourcepos(context, node)?;
|
|
125
|
+
context.write_str(" href=\"")?;
|
|
126
|
+
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
127
|
+
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
|
128
|
+
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
129
|
+
} else {
|
|
130
|
+
context.escape_href(&nl.url)?;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
context.write_str("\"")?;
|
|
134
|
+
|
|
135
|
+
if !nl.title.is_empty() {
|
|
136
|
+
context.write_str(" title=\"")?;
|
|
137
|
+
context.escape(&nl.title)?;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
141
|
+
context.write_str(" data-placeholder")?;
|
|
142
|
+
|
|
143
|
+
context.write_str(">")?;
|
|
144
|
+
} else {
|
|
145
|
+
context.write_str("</a>")?;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Ok(ChildRendering::HTML)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn render_image(
|
|
153
|
+
context: &mut Context<RenderUserData>,
|
|
154
|
+
node: Node<'_>,
|
|
155
|
+
entering: bool,
|
|
156
|
+
nl: &NodeLink,
|
|
157
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
158
|
+
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
159
|
+
return html::format_node_default(context, node, entering);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if entering {
|
|
163
|
+
if context.options.render.figure_with_caption {
|
|
164
|
+
context.write_str("<figure>")?;
|
|
165
|
+
}
|
|
166
|
+
context.write_str("<img")?;
|
|
167
|
+
html::render_sourcepos(context, node)?;
|
|
168
|
+
context.write_str(" src=\"")?;
|
|
169
|
+
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
170
|
+
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
|
|
171
|
+
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
172
|
+
} else {
|
|
173
|
+
context.escape_href(&nl.url)?;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
context.write_str("\"")?;
|
|
178
|
+
|
|
179
|
+
// This path only taken if placeholder detection is enabled, and the regex matched.
|
|
180
|
+
context.write_str(" data-placeholder")?;
|
|
181
|
+
|
|
182
|
+
context.write_str(" alt=\"")?;
|
|
183
|
+
|
|
184
|
+
return Ok(ChildRendering::Plain);
|
|
185
|
+
} else {
|
|
186
|
+
if !nl.title.is_empty() {
|
|
187
|
+
context.write_str("\" title=\"")?;
|
|
188
|
+
context.escape(&nl.title)?;
|
|
189
|
+
}
|
|
190
|
+
context.write_str("\" />")?;
|
|
191
|
+
if context.options.render.figure_with_caption {
|
|
192
|
+
if !nl.title.is_empty() {
|
|
193
|
+
context.write_str("<figcaption>")?;
|
|
194
|
+
context.escape(&nl.title)?;
|
|
195
|
+
context.write_str("</figcaption>")?;
|
|
196
|
+
}
|
|
197
|
+
context.write_str("</figure>")?;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Ok(ChildRendering::HTML)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Overridden to use class `task-list` instead of `contains-task-list`
|
|
205
|
+
// to align with GitLab class usage
|
|
206
|
+
fn render_list(
|
|
207
|
+
context: &mut Context<RenderUserData>,
|
|
208
|
+
node: Node<'_>,
|
|
209
|
+
entering: bool,
|
|
210
|
+
nl: &NodeList,
|
|
211
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
212
|
+
if !entering || !context.options.render.tasklist_classes {
|
|
213
|
+
return html::format_node_default(context, node, entering);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
context.cr()?;
|
|
217
|
+
match nl.list_type {
|
|
218
|
+
ListType::Bullet => {
|
|
219
|
+
context.write_str("<ul")?;
|
|
220
|
+
if nl.is_task_list {
|
|
221
|
+
context.write_str(" class=\"task-list\"")?;
|
|
222
|
+
}
|
|
223
|
+
html::render_sourcepos(context, node)?;
|
|
224
|
+
context.write_str(">\n")?;
|
|
225
|
+
}
|
|
226
|
+
ListType::Ordered => {
|
|
227
|
+
context.write_str("<ol")?;
|
|
228
|
+
if nl.is_task_list {
|
|
229
|
+
context.write_str(" class=\"task-list\"")?;
|
|
230
|
+
}
|
|
231
|
+
html::render_sourcepos(context, node)?;
|
|
232
|
+
if nl.start == 1 {
|
|
233
|
+
context.write_str(">\n")?;
|
|
234
|
+
} else {
|
|
235
|
+
writeln!(context, " start=\"{}\">", nl.start)?;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Ok(ChildRendering::HTML)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Overridden to:
|
|
244
|
+
// 1. Detect inapplicable task list items; and,
|
|
245
|
+
// 2. Output checkbox sourcepos.
|
|
246
|
+
fn render_task_item(
|
|
247
|
+
context: &mut Context<RenderUserData>,
|
|
248
|
+
node: Node<'_>,
|
|
249
|
+
entering: bool,
|
|
250
|
+
nti: &NodeTaskItem,
|
|
251
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
252
|
+
let write_li = node
|
|
253
|
+
.parent()
|
|
254
|
+
.map(|p| node_matches!(p, NodeValue::List(_)))
|
|
255
|
+
.unwrap_or_default();
|
|
256
|
+
|
|
257
|
+
if !entering {
|
|
258
|
+
if write_li {
|
|
259
|
+
context.write_str("</li>\n")?;
|
|
260
|
+
}
|
|
261
|
+
return Ok(ChildRendering::HTML);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
match nti.symbol {
|
|
265
|
+
Some('~') => {
|
|
266
|
+
render_task_item_with(
|
|
267
|
+
context,
|
|
268
|
+
node,
|
|
269
|
+
nti,
|
|
270
|
+
if context.user.inapplicable_tasks {
|
|
271
|
+
TaskItemState::Inapplicable
|
|
272
|
+
} else {
|
|
273
|
+
TaskItemState::Checked
|
|
274
|
+
},
|
|
275
|
+
write_li,
|
|
276
|
+
)?;
|
|
277
|
+
}
|
|
278
|
+
None => {
|
|
279
|
+
render_task_item_with(context, node, nti, TaskItemState::Unchecked, write_li)?;
|
|
280
|
+
}
|
|
281
|
+
Some(ws) if ws.is_whitespace() => {
|
|
282
|
+
render_task_item_with(context, node, nti, TaskItemState::Unchecked, write_li)?;
|
|
283
|
+
}
|
|
284
|
+
Some('x' | 'X') => {
|
|
285
|
+
render_task_item_with(context, node, nti, TaskItemState::Checked, write_li)?;
|
|
286
|
+
}
|
|
287
|
+
Some(symbol) => {
|
|
288
|
+
context.cr()?;
|
|
289
|
+
if write_li {
|
|
290
|
+
context.write_str("<li")?;
|
|
291
|
+
if context.options.render.tasklist_classes {
|
|
292
|
+
context.write_str(" class=\"task-list-item\"")?;
|
|
293
|
+
}
|
|
294
|
+
html::render_sourcepos(context, node)?;
|
|
295
|
+
context.write_str(">")?;
|
|
296
|
+
}
|
|
297
|
+
context.write_str("[")?;
|
|
298
|
+
context.escape(&symbol.to_string())?;
|
|
299
|
+
context.write_str("] ")?;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Ok(ChildRendering::HTML)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[derive(PartialEq)]
|
|
307
|
+
enum TaskItemState {
|
|
308
|
+
Checked,
|
|
309
|
+
Unchecked,
|
|
310
|
+
Inapplicable,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn render_task_item_with(
|
|
314
|
+
context: &mut Context<RenderUserData>,
|
|
315
|
+
node: Node<'_>,
|
|
316
|
+
nti: &NodeTaskItem,
|
|
317
|
+
state: TaskItemState,
|
|
318
|
+
write_li: bool,
|
|
319
|
+
) -> fmt::Result {
|
|
320
|
+
context.cr()?;
|
|
321
|
+
if write_li {
|
|
322
|
+
context.write_str("<li")?;
|
|
323
|
+
if context.options.render.tasklist_classes {
|
|
324
|
+
context.write_str(" class=\"")?;
|
|
325
|
+
if state == TaskItemState::Inapplicable {
|
|
326
|
+
context.write_str("inapplicable ")?;
|
|
327
|
+
}
|
|
328
|
+
context.write_str("task-list-item\"")?;
|
|
329
|
+
}
|
|
330
|
+
render_sourcepos(context, node)?;
|
|
331
|
+
context.write_str(">")?;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
context.write_str("<input type=\"checkbox\"")?;
|
|
335
|
+
if !write_li {
|
|
336
|
+
html::render_sourcepos(context, node)?;
|
|
337
|
+
}
|
|
338
|
+
if context.options.render.sourcepos {
|
|
339
|
+
write!(
|
|
340
|
+
context,
|
|
341
|
+
" data-checkbox-sourcepos=\"{:?}\"",
|
|
342
|
+
nti.symbol_sourcepos
|
|
343
|
+
)?;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if context.options.render.tasklist_classes {
|
|
347
|
+
context.write_str(" class=\"task-list-item-checkbox\"")?;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
match state {
|
|
351
|
+
TaskItemState::Checked => {
|
|
352
|
+
context.write_str(" checked=\"\"")?;
|
|
353
|
+
}
|
|
354
|
+
TaskItemState::Unchecked => {}
|
|
355
|
+
TaskItemState::Inapplicable => {
|
|
356
|
+
context.write_str(" data-inapplicable")?;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
context.write_str(" disabled=\"\" /> ")?;
|
|
360
|
+
|
|
361
|
+
Ok(())
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn render_escaped(
|
|
365
|
+
context: &mut Context<RenderUserData>,
|
|
366
|
+
node: Node<'_>,
|
|
367
|
+
entering: bool,
|
|
368
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
369
|
+
if !context.options.render.escaped_char_spans {
|
|
370
|
+
return Ok(ChildRendering::HTML);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if context
|
|
374
|
+
.user
|
|
375
|
+
.only_escape_chars
|
|
376
|
+
.as_ref()
|
|
377
|
+
.is_none_or(|only_escape_chars| {
|
|
378
|
+
with_node_text_content(node, false, |content| {
|
|
379
|
+
content.chars().count() == 1
|
|
380
|
+
&& only_escape_chars.contains(&content.chars().next().unwrap())
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
{
|
|
384
|
+
if entering {
|
|
385
|
+
context.write_str("<span data-escaped-char")?;
|
|
386
|
+
render_sourcepos(context, node)?;
|
|
387
|
+
context.write_str(">")?;
|
|
388
|
+
} else {
|
|
389
|
+
context.write_str("</span>")?;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Ok(ChildRendering::HTML)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn render_heading(
|
|
397
|
+
context: &mut Context<RenderUserData>,
|
|
398
|
+
node: Node<'_>,
|
|
399
|
+
entering: bool,
|
|
400
|
+
nh: &NodeHeading,
|
|
401
|
+
) -> Result<ChildRendering, fmt::Error> {
|
|
402
|
+
if !context.user.header_accessibility || context.plugins.render.heading_adapter.is_some() {
|
|
403
|
+
return format_node_default(context, node, entering);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if entering {
|
|
407
|
+
context.cr()?;
|
|
408
|
+
write!(context, "<h{}", nh.level)?;
|
|
409
|
+
|
|
410
|
+
if let Some(ref prefix) = context.options.extension.header_ids {
|
|
411
|
+
let text_content = collect_text(node);
|
|
412
|
+
let id = context.anchorizer.anchorize(&text_content);
|
|
413
|
+
write!(context, r##" id="{prefix}{id}""##)?;
|
|
414
|
+
context.user.last_heading = Some(id);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
render_sourcepos(context, node)?;
|
|
418
|
+
context.write_str(">")?;
|
|
419
|
+
} else {
|
|
420
|
+
if context.options.extension.header_ids.is_some() {
|
|
421
|
+
let id = context.user.last_heading.take().unwrap();
|
|
422
|
+
let text_content = collect_text(node);
|
|
423
|
+
write!(
|
|
424
|
+
context,
|
|
425
|
+
r##"<a href="#{id}" aria-label="Link to heading '"##
|
|
426
|
+
)?;
|
|
427
|
+
context.escape(&text_content)?;
|
|
428
|
+
context.write_str(r#"'" data-heading-content=""#)?;
|
|
429
|
+
context.escape(&text_content)?;
|
|
430
|
+
context.write_str(r#"" class="anchor"></a>"#)?;
|
|
431
|
+
}
|
|
432
|
+
writeln!(context, "</h{}>", nh.level)?;
|
|
433
|
+
}
|
|
434
|
+
Ok(ChildRendering::HTML)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// If the given node has a single text child, apply a function to the text content
|
|
438
|
+
/// of that node. Otherwise, return the given default value.
|
|
439
|
+
fn with_node_text_content<U, F>(node: Node<'_>, default: U, f: F) -> U
|
|
440
|
+
where
|
|
441
|
+
F: FnOnce(&str) -> U,
|
|
442
|
+
{
|
|
443
|
+
let Some(child) = node.first_child() else {
|
|
444
|
+
return default;
|
|
445
|
+
};
|
|
446
|
+
let Some(last_child) = node.last_child() else {
|
|
447
|
+
return default;
|
|
448
|
+
};
|
|
449
|
+
if !child.same_node(last_child) {
|
|
450
|
+
return default;
|
|
451
|
+
}
|
|
452
|
+
let NodeValue::Text(ref text) = child.data.borrow().value else {
|
|
453
|
+
return default;
|
|
454
|
+
};
|
|
455
|
+
f(text)
|
|
456
|
+
}
|
|
@@ -10,6 +10,8 @@ pub struct RenderOptions {
|
|
|
10
10
|
pub alerts: bool,
|
|
11
11
|
pub autolink: bool,
|
|
12
12
|
pub cjk_friendly_emphasis: bool,
|
|
13
|
+
/// Only use default comrak HTML formatting
|
|
14
|
+
pub default_html: bool,
|
|
13
15
|
// pub default_info_string: String,
|
|
14
16
|
pub description_lists: bool,
|
|
15
17
|
pub escape: bool,
|
|
@@ -23,16 +25,27 @@ pub struct RenderOptions {
|
|
|
23
25
|
pub github_pre_lang: bool,
|
|
24
26
|
pub greentext: bool,
|
|
25
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,
|
|
26
31
|
pub header_ids: Option<String>,
|
|
27
32
|
pub ignore_empty_links: bool,
|
|
28
33
|
pub ignore_setext: bool,
|
|
34
|
+
/// Detect inapplicable tasks (`- [~]`)
|
|
35
|
+
pub inapplicable_tasks: bool,
|
|
29
36
|
pub math_code: bool,
|
|
30
37
|
pub math_dollars: bool,
|
|
31
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,
|
|
32
45
|
pub relaxed_autolinks: bool,
|
|
33
46
|
pub relaxed_tasklist_character: bool,
|
|
34
|
-
pub sourcepos: bool,
|
|
35
47
|
pub smart: bool,
|
|
48
|
+
pub sourcepos: bool,
|
|
36
49
|
pub spoiler: bool,
|
|
37
50
|
pub strikethrough: bool,
|
|
38
51
|
pub subscript: bool,
|
|
@@ -42,40 +55,11 @@ pub struct RenderOptions {
|
|
|
42
55
|
pub tagfilter: bool,
|
|
43
56
|
pub tasklist: bool,
|
|
44
57
|
pub tasklist_classes: bool,
|
|
58
|
+
pub tasklist_in_table: bool,
|
|
45
59
|
pub underline: bool,
|
|
46
60
|
pub r#unsafe: bool,
|
|
47
61
|
pub wikilinks_title_after_pipe: bool,
|
|
48
62
|
pub wikilinks_title_before_pipe: bool,
|
|
49
|
-
|
|
50
|
-
/// GLFM specific options
|
|
51
|
-
|
|
52
|
-
/// Only use default comrak HTML formatting
|
|
53
|
-
pub default_html: bool,
|
|
54
|
-
|
|
55
|
-
/// Detect inapplicable tasks (`- [~]`)
|
|
56
|
-
pub inapplicable_tasks: bool,
|
|
57
|
-
|
|
58
|
-
/// Detect and mark potential placeholder variables, which
|
|
59
|
-
/// have the format `%{PLACEHOLDER}`
|
|
60
|
-
pub placeholder_detection: bool,
|
|
61
|
-
|
|
62
|
-
/// When the 'escaped_char_spans' render option is enabled, only emit `<span
|
|
63
|
-
/// data-escaped-char>` around the given characters.
|
|
64
|
-
pub only_escape_chars: Option<Vec<char>>,
|
|
65
|
-
|
|
66
|
-
pub debug: bool,
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
impl From<&RenderOptions> for RenderUserData {
|
|
70
|
-
fn from(options: &RenderOptions) -> Self {
|
|
71
|
-
RenderUserData {
|
|
72
|
-
default_html: options.default_html,
|
|
73
|
-
inapplicable_tasks: options.inapplicable_tasks,
|
|
74
|
-
placeholder_detection: options.placeholder_detection,
|
|
75
|
-
only_escape_chars: options.only_escape_chars.clone(),
|
|
76
|
-
debug: options.debug,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
impl From<&RenderOptions> for comrak::Options<'_> {
|
|
@@ -120,10 +104,11 @@ impl From<&RenderOptions> for comrak::Options<'_> {
|
|
|
120
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;
|
|
126
|
-
comrak_options.parse.
|
|
111
|
+
comrak_options.parse.tasklist_in_table = options.tasklist_in_table;
|
|
127
112
|
|
|
128
113
|
comrak_options
|
|
129
114
|
}
|
|
@@ -14,6 +14,10 @@ struct Args {
|
|
|
14
14
|
#[arg(value_name = "FILE")]
|
|
15
15
|
file: Option<String>,
|
|
16
16
|
|
|
17
|
+
/// Write output to FILE instead of stdout
|
|
18
|
+
#[arg(short, long, value_name = "FILE")]
|
|
19
|
+
output: Option<String>,
|
|
20
|
+
|
|
17
21
|
/// Enable 'alerts' extension
|
|
18
22
|
#[arg(long)]
|
|
19
23
|
alerts: bool,
|
|
@@ -22,6 +26,14 @@ struct Args {
|
|
|
22
26
|
#[arg(long)]
|
|
23
27
|
autolink: bool,
|
|
24
28
|
|
|
29
|
+
/// Enable 'cjk_friendly_emphasis' extension
|
|
30
|
+
#[arg(long)]
|
|
31
|
+
cjk_friendly_emphasis: bool,
|
|
32
|
+
|
|
33
|
+
/// Only use default comrak HTML formatting
|
|
34
|
+
#[arg(long)]
|
|
35
|
+
default_html: bool,
|
|
36
|
+
|
|
25
37
|
/// Enable 'description-lists' extension
|
|
26
38
|
#[arg(long)]
|
|
27
39
|
description_lists: bool,
|
|
@@ -72,6 +84,11 @@ struct Args {
|
|
|
72
84
|
#[arg(long)]
|
|
73
85
|
hardbreaks: bool,
|
|
74
86
|
|
|
87
|
+
/// Use new header/anchor combination in HTML output, per
|
|
88
|
+
/// https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/merge_requests/112.
|
|
89
|
+
#[arg(long)]
|
|
90
|
+
header_accessibility: bool,
|
|
91
|
+
|
|
75
92
|
/// Enable the 'header IDs' extension, with the given ID prefix
|
|
76
93
|
#[arg(long, value_name = "PREFIX")]
|
|
77
94
|
header_ids: Option<String>,
|
|
@@ -84,6 +101,10 @@ struct Args {
|
|
|
84
101
|
#[arg(long)]
|
|
85
102
|
ignore_setext: bool,
|
|
86
103
|
|
|
104
|
+
/// Detect inapplicable tasks (`- [~]`)
|
|
105
|
+
#[arg(long)]
|
|
106
|
+
inapplicable_tasks: bool,
|
|
107
|
+
|
|
87
108
|
/// Enables `math code` extension, using math code syntax
|
|
88
109
|
#[arg(long)]
|
|
89
110
|
math_code: bool,
|
|
@@ -96,9 +117,15 @@ struct Args {
|
|
|
96
117
|
#[arg(long)]
|
|
97
118
|
multiline_block_quotes: bool,
|
|
98
119
|
|
|
99
|
-
///
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
/// When the 'escaped_char_spans' render option is enabled, only emit `<span
|
|
121
|
+
/// data-escaped-char>` around the characters in the supplied string.
|
|
122
|
+
#[arg(long)]
|
|
123
|
+
only_escape_chars: Option<String>,
|
|
124
|
+
|
|
125
|
+
/// Detect and marks potential placeholder variables, which
|
|
126
|
+
/// have the format `%{PLACEHOLDER}`
|
|
127
|
+
#[arg(long)]
|
|
128
|
+
placeholder_detection: bool,
|
|
102
129
|
|
|
103
130
|
/// Enable relaxing of autolink parsing, allowing links to be recognized when in brackets
|
|
104
131
|
#[arg(long)]
|
|
@@ -108,14 +135,14 @@ struct Args {
|
|
|
108
135
|
#[arg(long)]
|
|
109
136
|
relaxed_tasklist_character: bool,
|
|
110
137
|
|
|
111
|
-
/// Include source mappings in HTML attributes
|
|
112
|
-
#[arg(long)]
|
|
113
|
-
sourcepos: bool,
|
|
114
|
-
|
|
115
138
|
/// Use smart punctuation
|
|
116
139
|
#[arg(long)]
|
|
117
140
|
smart: bool,
|
|
118
141
|
|
|
142
|
+
/// Include source mappings in HTML attributes
|
|
143
|
+
#[arg(long)]
|
|
144
|
+
sourcepos: bool,
|
|
145
|
+
|
|
119
146
|
/// Enables spoilers using double vertical bars
|
|
120
147
|
#[arg(long)]
|
|
121
148
|
spoiler: bool,
|
|
@@ -152,6 +179,10 @@ struct Args {
|
|
|
152
179
|
#[arg(long)]
|
|
153
180
|
tasklist_classes: bool,
|
|
154
181
|
|
|
182
|
+
/// Enables parsing tasklist items within tables when they're the only content of a table cell
|
|
183
|
+
#[arg(long)]
|
|
184
|
+
tasklist_in_table: bool,
|
|
185
|
+
|
|
155
186
|
/// Enables underlines using double underscores
|
|
156
187
|
#[arg(long)]
|
|
157
188
|
underline: bool,
|
|
@@ -167,34 +198,6 @@ struct Args {
|
|
|
167
198
|
/// Enable 'wikilink_title_before_pipe' extension
|
|
168
199
|
#[arg(long)]
|
|
169
200
|
wikilinks_title_before_pipe: bool,
|
|
170
|
-
|
|
171
|
-
/// Only use default comrak HTML formatting
|
|
172
|
-
#[arg(long)]
|
|
173
|
-
default_html: bool,
|
|
174
|
-
|
|
175
|
-
/// GLFM specific options
|
|
176
|
-
|
|
177
|
-
/// Detect inapplicable tasks (`- [~]`)
|
|
178
|
-
#[arg(long)]
|
|
179
|
-
inapplicable_tasks: bool,
|
|
180
|
-
|
|
181
|
-
/// Detect and marks potential placeholder variables, which
|
|
182
|
-
/// have the format `%{PLACEHOLDER}`
|
|
183
|
-
#[arg(long)]
|
|
184
|
-
placeholder_detection: bool,
|
|
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
|
-
|
|
191
|
-
/// Enable 'cjk_friendly_emphasis' extension
|
|
192
|
-
#[arg(long)]
|
|
193
|
-
cjk_friendly_emphasis: bool,
|
|
194
|
-
|
|
195
|
-
/// Show debug information
|
|
196
|
-
#[arg(long)]
|
|
197
|
-
debug: bool,
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
fn main() {
|
|
@@ -216,6 +219,7 @@ fn main() {
|
|
|
216
219
|
autolink: cli.autolink,
|
|
217
220
|
cjk_friendly_emphasis: cli.cjk_friendly_emphasis,
|
|
218
221
|
// default_info_string:
|
|
222
|
+
default_html: cli.default_html,
|
|
219
223
|
description_lists: cli.description_lists,
|
|
220
224
|
escape: cli.escape,
|
|
221
225
|
escaped_char_spans: cli.escaped_char_spans,
|
|
@@ -228,6 +232,7 @@ fn main() {
|
|
|
228
232
|
github_pre_lang: cli.github_pre_lang,
|
|
229
233
|
greentext: cli.greentext,
|
|
230
234
|
hardbreaks: cli.hardbreaks,
|
|
235
|
+
header_accessibility: cli.header_accessibility,
|
|
231
236
|
header_ids: cli.header_ids,
|
|
232
237
|
ignore_empty_links: cli.ignore_empty_links,
|
|
233
238
|
ignore_setext: cli.ignore_setext,
|
|
@@ -235,10 +240,12 @@ fn main() {
|
|
|
235
240
|
math_code: cli.math_code,
|
|
236
241
|
math_dollars: cli.math_dollars,
|
|
237
242
|
multiline_block_quotes: cli.multiline_block_quotes,
|
|
243
|
+
only_escape_chars: cli.only_escape_chars.map(|s| s.chars().collect()),
|
|
244
|
+
placeholder_detection: cli.placeholder_detection,
|
|
238
245
|
relaxed_autolinks: cli.relaxed_autolinks,
|
|
239
246
|
relaxed_tasklist_character: cli.relaxed_tasklist_character,
|
|
240
|
-
sourcepos: cli.sourcepos,
|
|
241
247
|
smart: cli.smart,
|
|
248
|
+
sourcepos: cli.sourcepos,
|
|
242
249
|
spoiler: cli.spoiler,
|
|
243
250
|
strikethrough: cli.strikethrough,
|
|
244
251
|
subscript: cli.subscript,
|
|
@@ -248,15 +255,11 @@ fn main() {
|
|
|
248
255
|
tagfilter: cli.tagfilter,
|
|
249
256
|
tasklist: cli.tasklist,
|
|
250
257
|
tasklist_classes: cli.tasklist_classes,
|
|
258
|
+
tasklist_in_table: cli.tasklist_in_table,
|
|
251
259
|
underline: cli.underline,
|
|
252
260
|
r#unsafe: cli.r#unsafe,
|
|
253
261
|
wikilinks_title_after_pipe: cli.wikilinks_title_after_pipe,
|
|
254
262
|
wikilinks_title_before_pipe: cli.wikilinks_title_before_pipe,
|
|
255
|
-
|
|
256
|
-
default_html: cli.default_html,
|
|
257
|
-
placeholder_detection: cli.placeholder_detection,
|
|
258
|
-
only_escape_chars: cli.only_escape_chars.map(|s| s.chars().collect()),
|
|
259
|
-
debug: cli.debug,
|
|
260
263
|
};
|
|
261
264
|
|
|
262
265
|
let result = render(&source, &options);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative 'gitlab_glfm_markdown/version'
|
|
4
|
+
require_relative 'gitlab_glfm_markdown/loader'
|
|
5
5
|
|
|
6
6
|
load_rust_extension
|
|
7
7
|
|
|
@@ -27,9 +27,7 @@ module GLFMMarkdown
|
|
|
27
27
|
tagfilter: false,
|
|
28
28
|
tasklist: true,
|
|
29
29
|
tasklist_classes: true,
|
|
30
|
-
unsafe: true
|
|
31
|
-
|
|
32
|
-
debug: false
|
|
30
|
+
unsafe: true
|
|
33
31
|
}.freeze
|
|
34
32
|
|
|
35
33
|
class << self
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
def load_rust_extension
|
|
4
4
|
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
|
|
5
|
-
require "
|
|
5
|
+
require "gitlab_glfm_markdown/#{ruby_version}/gitlab_glfm_markdown"
|
|
6
6
|
rescue LoadError
|
|
7
|
-
require '
|
|
7
|
+
require 'gitlab_glfm_markdown/gitlab_glfm_markdown'
|
|
8
8
|
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.40
|
|
5
5
|
platform: ruby
|
|
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-10
|
|
12
|
+
date: 2025-12-10 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rb_sys
|
|
@@ -59,22 +59,22 @@ email:
|
|
|
59
59
|
- aconnor@gitlab.com
|
|
60
60
|
executables: []
|
|
61
61
|
extensions:
|
|
62
|
-
- ext/
|
|
62
|
+
- ext/gitlab_glfm_markdown/extconf.rb
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
65
|
- Cargo.lock
|
|
66
66
|
- LICENSE
|
|
67
67
|
- README.md
|
|
68
|
-
- ext/
|
|
69
|
-
- ext/
|
|
70
|
-
- ext/
|
|
71
|
-
- ext/
|
|
72
|
-
- ext/
|
|
73
|
-
- ext/
|
|
74
|
-
- ext/
|
|
75
|
-
- lib/
|
|
76
|
-
- lib/
|
|
77
|
-
- lib/
|
|
68
|
+
- ext/gitlab_glfm_markdown/Cargo.lock
|
|
69
|
+
- ext/gitlab_glfm_markdown/Cargo.toml
|
|
70
|
+
- ext/gitlab_glfm_markdown/extconf.rb
|
|
71
|
+
- ext/gitlab_glfm_markdown/src/formatter.rs
|
|
72
|
+
- ext/gitlab_glfm_markdown/src/glfm.rs
|
|
73
|
+
- ext/gitlab_glfm_markdown/src/lib.rs
|
|
74
|
+
- ext/gitlab_glfm_markdown/src/main.rs
|
|
75
|
+
- lib/gitlab-glfm-markdown.rb
|
|
76
|
+
- lib/gitlab_glfm_markdown/loader.rb
|
|
77
|
+
- lib/gitlab_glfm_markdown/version.rb
|
|
78
78
|
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown
|
|
79
79
|
licenses:
|
|
80
80
|
- MIT
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
use std::fmt::{self, Write};
|
|
2
|
-
|
|
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;
|
|
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::Escaped => |context, node, entering| {
|
|
47
|
-
return render_escaped(context, node, entering);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
fn render_image<'a>(
|
|
52
|
-
context: &mut Context<RenderUserData>,
|
|
53
|
-
node: &'a AstNode<'a>,
|
|
54
|
-
entering: bool,
|
|
55
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
56
|
-
let NodeValue::Image(ref nl) = node.data.borrow().value else {
|
|
57
|
-
unreachable!()
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
61
|
-
return html::format_node_default(context, node, entering);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if entering {
|
|
65
|
-
if context.options.render.figure_with_caption {
|
|
66
|
-
context.write_str("<figure>")?;
|
|
67
|
-
}
|
|
68
|
-
context.write_str("<img")?;
|
|
69
|
-
html::render_sourcepos(context, node)?;
|
|
70
|
-
context.write_str(" src=\"")?;
|
|
71
|
-
if context.options.render.r#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)?;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
context.write_str("\"")?;
|
|
80
|
-
|
|
81
|
-
if PLACEHOLDER_REGEX.is_match(&nl.url) {
|
|
82
|
-
context.write_str(" data-placeholder")?;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
context.write_str(" alt=\"")?;
|
|
86
|
-
|
|
87
|
-
return Ok(ChildRendering::Plain);
|
|
88
|
-
} else {
|
|
89
|
-
if !nl.title.is_empty() {
|
|
90
|
-
context.write_str("\" title=\"")?;
|
|
91
|
-
context.escape(&nl.title)?;
|
|
92
|
-
}
|
|
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
|
-
}
|
|
103
|
-
|
|
104
|
-
Ok(ChildRendering::HTML)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
fn render_link<'a>(
|
|
108
|
-
context: &mut Context<RenderUserData>,
|
|
109
|
-
node: &'a AstNode<'a>,
|
|
110
|
-
entering: bool,
|
|
111
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
112
|
-
let NodeValue::Link(ref nl) = node.data.borrow().value else {
|
|
113
|
-
unreachable!()
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(&nl.url)) {
|
|
117
|
-
return html::format_node_default(context, node, entering);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let parent_node = node.parent();
|
|
121
|
-
|
|
122
|
-
if !context.options.parse.relaxed_autolinks
|
|
123
|
-
|| (parent_node.is_none()
|
|
124
|
-
|| !matches!(
|
|
125
|
-
parent_node.unwrap().data.borrow().value,
|
|
126
|
-
NodeValue::Link(..)
|
|
127
|
-
))
|
|
128
|
-
{
|
|
129
|
-
if entering {
|
|
130
|
-
context.write_str("<a")?;
|
|
131
|
-
html::render_sourcepos(context, node)?;
|
|
132
|
-
context.write_str(" href=\"")?;
|
|
133
|
-
if context.options.render.r#unsafe || !html::dangerous_url(&nl.url) {
|
|
134
|
-
if let Some(rewriter) = &context.options.extension.link_url_rewriter {
|
|
135
|
-
context.escape_href(&rewriter.to_html(&nl.url))?;
|
|
136
|
-
} else {
|
|
137
|
-
context.escape_href(&nl.url)?;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
context.write_str("\"")?;
|
|
141
|
-
|
|
142
|
-
if !nl.title.is_empty() {
|
|
143
|
-
context.write_str(" title=\"")?;
|
|
144
|
-
context.escape(&nl.title)?;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if PLACEHOLDER_REGEX.is_match(&nl.url) {
|
|
148
|
-
context.write_str(" data-placeholder")?;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
context.write_str(">")?;
|
|
152
|
-
} else {
|
|
153
|
-
context.write_str("</a>")?;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
Ok(ChildRendering::HTML)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Overridden to use class `task-list` instead of `contains-task-list`
|
|
161
|
-
// to align with GitLab class usage
|
|
162
|
-
fn render_list<'a>(
|
|
163
|
-
context: &mut Context<RenderUserData>,
|
|
164
|
-
node: &'a AstNode<'a>,
|
|
165
|
-
entering: bool,
|
|
166
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
167
|
-
if !entering || !context.options.render.tasklist_classes {
|
|
168
|
-
return html::format_node_default(context, node, entering);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let NodeValue::List(ref nl) = node.data.borrow().value else {
|
|
172
|
-
unreachable!()
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
context.cr()?;
|
|
176
|
-
match nl.list_type {
|
|
177
|
-
ListType::Bullet => {
|
|
178
|
-
context.write_str("<ul")?;
|
|
179
|
-
if nl.is_task_list {
|
|
180
|
-
context.write_str(" class=\"task-list\"")?;
|
|
181
|
-
}
|
|
182
|
-
html::render_sourcepos(context, node)?;
|
|
183
|
-
context.write_str(">\n")?;
|
|
184
|
-
}
|
|
185
|
-
ListType::Ordered => {
|
|
186
|
-
context.write_str("<ol")?;
|
|
187
|
-
if nl.is_task_list {
|
|
188
|
-
context.write_str(" class=\"task-list\"")?;
|
|
189
|
-
}
|
|
190
|
-
html::render_sourcepos(context, node)?;
|
|
191
|
-
if nl.start == 1 {
|
|
192
|
-
context.write_str(">\n")?;
|
|
193
|
-
} else {
|
|
194
|
-
writeln!(context, " start=\"{}\">", nl.start)?;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
Ok(ChildRendering::HTML)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Overridden to detect inapplicable task list items
|
|
203
|
-
fn render_task_item<'a>(
|
|
204
|
-
context: &mut Context<RenderUserData>,
|
|
205
|
-
node: &'a AstNode<'a>,
|
|
206
|
-
entering: bool,
|
|
207
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
208
|
-
if !context.user.inapplicable_tasks {
|
|
209
|
-
return html::format_node_default(context, node, entering);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
let NodeValue::TaskItem(symbol) = node.data.borrow().value else {
|
|
213
|
-
unreachable!()
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
if symbol.is_none() || matches!(symbol, Some('x' | 'X')) {
|
|
217
|
-
return html::format_node_default(context, node, entering);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if entering {
|
|
221
|
-
// Handle an inapplicable task symbol.
|
|
222
|
-
if matches!(symbol, Some('~')) {
|
|
223
|
-
context.cr()?;
|
|
224
|
-
context.write_str("<li")?;
|
|
225
|
-
context.write_str(" class=\"inapplicable")?;
|
|
226
|
-
|
|
227
|
-
if context.options.render.tasklist_classes {
|
|
228
|
-
context.write_str(" task-list-item")?;
|
|
229
|
-
}
|
|
230
|
-
context.write_str("\"")?;
|
|
231
|
-
|
|
232
|
-
html::render_sourcepos(context, node)?;
|
|
233
|
-
context.write_str(">")?;
|
|
234
|
-
context.write_str("<input type=\"checkbox\"")?;
|
|
235
|
-
|
|
236
|
-
if context.options.render.tasklist_classes {
|
|
237
|
-
context.write_str(" class=\"task-list-item-checkbox\"")?;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
context.write_str(" data-inapplicable disabled=\"\"> ")?;
|
|
241
|
-
} else {
|
|
242
|
-
// Don't allow unsupported symbols to render a checkbox
|
|
243
|
-
context.cr()?;
|
|
244
|
-
context.write_str("<li")?;
|
|
245
|
-
|
|
246
|
-
if context.options.render.tasklist_classes {
|
|
247
|
-
context.write_str(" class=\"task-list-item\"")?;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
html::render_sourcepos(context, node)?;
|
|
251
|
-
context.write_str(">")?;
|
|
252
|
-
context.write_str("[")?;
|
|
253
|
-
context.escape(&symbol.unwrap().to_string())?;
|
|
254
|
-
context.write_str("] ")?;
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
context.write_str("</li>\n")?;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
Ok(ChildRendering::HTML)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
fn render_text<'a>(
|
|
264
|
-
context: &mut Context<RenderUserData>,
|
|
265
|
-
node: &'a AstNode<'a>,
|
|
266
|
-
entering: bool,
|
|
267
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
268
|
-
let NodeValue::Text(ref literal) = node.data.borrow().value else {
|
|
269
|
-
unreachable!()
|
|
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);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if entering {
|
|
290
|
-
let mut cursor: usize = 0;
|
|
291
|
-
|
|
292
|
-
for mat in PLACEHOLDER_REGEX.find_iter(literal) {
|
|
293
|
-
if mat.start() > cursor {
|
|
294
|
-
context.escape(&literal[cursor..mat.start()])?;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
context.write_str("<span data-placeholder>")?;
|
|
298
|
-
context.escape(&literal[mat.start()..mat.end()])?;
|
|
299
|
-
context.write_str("</span>")?;
|
|
300
|
-
|
|
301
|
-
cursor = mat.end();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if cursor < literal.len() {
|
|
305
|
-
context.escape(&literal[cursor..literal.len()])?;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
Ok(ChildRendering::HTML)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
fn render_escaped<'a>(
|
|
313
|
-
context: &mut Context<RenderUserData>,
|
|
314
|
-
node: &'a AstNode<'a>,
|
|
315
|
-
entering: bool,
|
|
316
|
-
) -> Result<ChildRendering, fmt::Error> {
|
|
317
|
-
if !context.options.render.escaped_char_spans {
|
|
318
|
-
return Ok(ChildRendering::HTML);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if context.user.only_escape_chars.is_none()
|
|
322
|
-
|| with_node_text_content(node, false, |content| {
|
|
323
|
-
if content.len() != 1 {
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
let c = content.chars().next().unwrap();
|
|
327
|
-
context
|
|
328
|
-
.user
|
|
329
|
-
.only_escape_chars
|
|
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>")?;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
Ok(ChildRendering::HTML)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/// If the given node has a single text child, apply a function to the text content
|
|
348
|
-
/// of that node. Otherwise, return the given default value.
|
|
349
|
-
fn with_node_text_content<'a, U, F>(node: &'a AstNode<'a>, default: U, f: F) -> U
|
|
350
|
-
where
|
|
351
|
-
F: FnOnce(&str) -> U,
|
|
352
|
-
{
|
|
353
|
-
let Some(child) = node.first_child() else {
|
|
354
|
-
return default;
|
|
355
|
-
};
|
|
356
|
-
let Some(last_child) = node.last_child() else {
|
|
357
|
-
return default;
|
|
358
|
-
};
|
|
359
|
-
if !child.same_node(last_child) {
|
|
360
|
-
return default;
|
|
361
|
-
}
|
|
362
|
-
let NodeValue::Text(ref text) = child.data.borrow().value else {
|
|
363
|
-
return default;
|
|
364
|
-
};
|
|
365
|
-
f(text)
|
|
366
|
-
}
|
|
File without changes
|
|
File without changes
|