gitlab-glfm-markdown 0.0.38-x86_64-linux-gnu → 0.0.40-x86_64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba881ded72921aedae7bbda8916417b7e850f196b1aead039d55ab167833edfc
4
- data.tar.gz: f73354bfef3a9be98d22a57c694d12377277150897ec6f7482b2ad001c282b4c
3
+ metadata.gz: 727000a57ece814d001538de227bc994adf468b4c2123e46149e6712a8aac4c2
4
+ data.tar.gz: 5f811b021a68d56815164eefe376ea71dc0b7b4d34e2b79214759b06d6a2e506
5
5
  SHA512:
6
- metadata.gz: 23b18b486e6f3afbd44a1f485fcde08601ebbab1df9e13540b23c8ea22e6c8ace3f178b50687a0d440b71c489bde5bd0b62eabbc52c670abeeb8dbae240424f6
7
- data.tar.gz: 57494b03e5b71df8b240376544c5e6c651a5e82fc6f593e162b241eceffed44ac856e5a8bcf339f1153e9e11ea2710bf8f8f88853d88910b4f155cad683b2226
6
+ metadata.gz: 5b6a8d2636d11dbdd52f6ba5971b936b480242c2715cfc0a2355ecf39448d9c16a0085ac2c9102d6ee32f88104c84de0a292dad0b92f9b33d3b1c598d9bbe586
7
+ data.tar.gz: 3eb01a17583b2373a8b32b8d2b81b55bbfd0b50ae5e43ee97abf548033d851d9e842c84a15150e82694c29d6b8018102f7e2ecc5049bfae7f2e9ddba371b6a17
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
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.46.0"
224
+ version = "0.49.0"
225
225
  source = "registry+https://github.com/rust-lang/crates.io-index"
226
- checksum = "dc151d9c800c4fadd542c949b84c1dae6d1b424315ebcfd154e611d4f7910a97"
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 = "glfm_markdown"
310
- version = "0.0.38"
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
  [![Latest Release](https://gitlab.com/gitlab-org/ruby/gems/gitlab-glfm-markdown/-/badges/release.svg)](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.46.0), providing a Ruby interface.
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 'glfm_markdown'
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
- | `default_html` | Disables any custom HTML, and returns default HTML from `comrak` |
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 glfm_markdown --features="cli" -- --help
89
- cargo run --bin glfm_markdown --features="cli" -- --sourcepos
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
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.46.0"
224
+ version = "0.49.0"
225
225
  source = "registry+https://github.com/rust-lang/crates.io-index"
226
- checksum = "dc151d9c800c4fadd542c949b84c1dae6d1b424315ebcfd154e611d4f7910a97"
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 = "glfm_markdown"
310
- version = "0.0.38"
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 = "glfm_markdown"
3
- version = "0.0.38"
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 = "glfm_markdown"
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.46.0", default-features = false, features = ["shortcodes"] }
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('glfm_markdown/glfm_markdown') do |r|
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
+ }