gitlab-glfm-markdown 0.0.38-x86_64-linux-musl → 0.0.40-x86_64-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 +5 -5
- data/README.md +11 -9
- data/ext/{glfm_markdown → gitlab_glfm_markdown}/Cargo.lock +5 -5
- 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/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/ext/glfm_markdown/src/formatter.rs +0 -366
- data/lib/glfm_markdown/3.1/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.2/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.3/glfm_markdown.so +0 -0
- data/lib/glfm_markdown/3.4/glfm_markdown.so +0 -0
- /data/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: eb0088abff5e397bde9577a878cd2c825661abe6d2dc692e0a251fa9fcefae86
|
|
4
|
+
data.tar.gz: 5dd36aa2212ee06c4d1546dc4137753614561a660a67c7fd93bb7dd416b981d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8ff221a2a371e4de120f7d93cb87ccd1f52670e19faa3e40cc79e0c71b4c26e70b6b04e60680d6d9102b00b068f0fa194114c7c18f8caa68f0e1c0f294eaee2
|
|
7
|
+
data.tar.gz: fa4678a099c510324ebd91d4533f0cfcf0495f8d81945d4d463850c564f8c14341bceea8c3abe24898cd46ad36dc751865644f94aee0cc4d082b3056b58d89cb
|
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
|
# 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",
|
|
@@ -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
|
+
}
|