gitlab-glfm-markdown 0.0.29-aarch64-linux-musl → 0.0.31-aarch64-linux-musl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "glfm_markdown"
3
- version = "0.0.29"
3
+ version = "0.0.31"
4
4
  edition = "2021"
5
5
  authors = ["digitalmoksha <bwalker@gitlab.com>"]
6
6
  description = "GitLab Flavored Markdown parser and formatter. 100% CommonMark-compatible. Experimental."
@@ -14,10 +14,12 @@ name = "glfm_markdown"
14
14
  required-features = ["cli"]
15
15
 
16
16
  [dependencies]
17
- clap = { version = "4.0", optional = true, features = ["derive", "string"] }
18
- comrak = { version = "0.38.0", default-features = false, features = ["shortcodes"] }
17
+ clap = { version = "=4.4.18", optional = true, features = ["derive", "string"] }
18
+ comrak = { version = "0.39.0", default-features = false, features = ["shortcodes"] }
19
19
  magnus = "0.6.2"
20
20
  rb-sys = { version = "0.9.86", default-features = false, features = ["stable-api-compiled-fallback"] }
21
+ regex = "1.11.1"
22
+ lazy_static = "1.5.0"
21
23
 
22
24
  [features]
23
25
  cli = ["clap", "comrak/syntect"]
@@ -5,4 +5,6 @@ require 'rb_sys/mkmf'
5
5
 
6
6
  create_rust_makefile('glfm_markdown/glfm_markdown') do |r|
7
7
  r.auto_install_rust_toolchain = false
8
+ # Ensure all Rust dependencies are pinned when building
9
+ r.extra_cargo_args = ["--locked"]
8
10
  end
@@ -1,4 +1,16 @@
1
- #[derive(Debug)]
1
+ use comrak::html::{ChildRendering, Context};
2
+ use comrak::nodes::{AstNode, NodeValue};
3
+ use comrak::{create_formatter, html, parse_document, Arena, Plugins};
4
+ use lazy_static::lazy_static;
5
+ use regex::Regex;
6
+ use std::io;
7
+ use std::io::{BufWriter, Write};
8
+
9
+ lazy_static! {
10
+ static ref PLACEHOLDER_REGEX: Regex = Regex::new(r"%(\{|%7B)(\w{1,30})(}|%7D)").unwrap();
11
+ }
12
+
13
+ #[derive(Debug, Clone)]
2
14
  pub struct RenderOptions {
3
15
  pub alerts: bool,
4
16
  pub autolink: bool,
@@ -38,56 +50,277 @@ pub struct RenderOptions {
38
50
  pub wikilinks_title_after_pipe: bool,
39
51
  pub wikilinks_title_before_pipe: bool,
40
52
 
53
+ /// Only use default comrak HTML formatting
54
+ pub default_html: bool,
55
+
56
+ /// Detect and mark potential placeholder variables, which
57
+ /// have the format `%{PLACEHOLDER}`
58
+ pub placeholder_detection: bool,
59
+
41
60
  pub debug: bool,
42
61
  }
43
62
 
63
+ pub struct RenderUserData {
64
+ pub default_html: bool,
65
+ pub placeholder_detection: bool,
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
+ placeholder_detection: options.placeholder_detection,
74
+ debug: options.debug,
75
+ }
76
+ }
77
+ }
78
+
79
+ impl From<&RenderOptions> for comrak::Options<'_> {
80
+ fn from(options: &RenderOptions) -> Self {
81
+ let mut comrak_options = comrak::Options::default();
82
+
83
+ comrak_options.extension.alerts = options.alerts;
84
+ comrak_options.extension.autolink = options.autolink;
85
+ comrak_options.extension.description_lists = options.description_lists;
86
+ comrak_options.extension.footnotes = options.footnotes;
87
+ // comrak_options.extension.front_matter_delimiter = options.front_matter_delimiter;
88
+ comrak_options.extension.greentext = options.greentext;
89
+ comrak_options.extension.header_ids = options.header_ids.clone();
90
+ comrak_options.extension.math_code = options.math_code;
91
+ comrak_options.extension.math_dollars = options.math_dollars;
92
+ comrak_options.extension.multiline_block_quotes = options.multiline_block_quotes;
93
+ comrak_options.extension.shortcodes = options.gemojis;
94
+ comrak_options.extension.spoiler = options.spoiler;
95
+ comrak_options.extension.strikethrough = options.strikethrough;
96
+ comrak_options.extension.subscript = options.subscript;
97
+ comrak_options.extension.superscript = options.superscript;
98
+ comrak_options.extension.table = options.table;
99
+ comrak_options.extension.tagfilter = options.tagfilter;
100
+ comrak_options.extension.tasklist = options.tasklist;
101
+ comrak_options.extension.underline = options.underline;
102
+ comrak_options.extension.wikilinks_title_after_pipe = options.wikilinks_title_after_pipe;
103
+ comrak_options.extension.wikilinks_title_before_pipe = options.wikilinks_title_before_pipe;
104
+
105
+ comrak_options.render.escape = options.escape;
106
+ comrak_options.render.escaped_char_spans = options.escaped_char_spans;
107
+ comrak_options.render.figure_with_caption = options.figure_with_caption;
108
+ comrak_options.render.full_info_string = options.full_info_string;
109
+ comrak_options.render.gfm_quirks = options.gfm_quirks;
110
+ comrak_options.render.github_pre_lang = options.github_pre_lang;
111
+ comrak_options.render.hardbreaks = options.hardbreaks;
112
+ comrak_options.render.ignore_empty_links = options.ignore_empty_links;
113
+ comrak_options.render.ignore_setext = options.ignore_setext;
114
+ comrak_options.render.sourcepos = options.sourcepos;
115
+ // comrak_options.render.syntax_highlighting = options.syntax_highlighting;
116
+
117
+ comrak_options.render.unsafe_ = options.unsafe_;
118
+
119
+ // comrak_options.parse.default_info_string = options.default_info_string;
120
+ comrak_options.parse.relaxed_autolinks = options.relaxed_autolinks;
121
+ comrak_options.parse.relaxed_tasklist_matching = options.relaxed_tasklist_character;
122
+ comrak_options.parse.smart = options.smart;
123
+
124
+ comrak_options
125
+ }
126
+ }
127
+
44
128
  pub fn render(text: String, options: RenderOptions) -> String {
45
- render_comrak(text, options)
129
+ render_with_plugins(text, options, &comrak::Plugins::default())
130
+ }
131
+
132
+ fn render_with_plugins(text: String, render_options: RenderOptions, plugins: &Plugins) -> String {
133
+ let user_data = RenderUserData::from(&render_options);
134
+ let options = comrak::Options::from(&render_options);
135
+
136
+ if user_data.default_html {
137
+ return comrak::markdown_to_html_with_plugins(&text, &options, plugins);
138
+ }
139
+
140
+ let arena = Arena::new();
141
+ let root = parse_document(&arena, &text, &options);
142
+ let mut bw = BufWriter::new(Vec::new());
143
+
144
+ CustomFormatter::format_document_with_plugins(root, &options, &mut bw, plugins, user_data)
145
+ .unwrap();
146
+ String::from_utf8(bw.into_inner().unwrap()).unwrap()
46
147
  }
47
148
 
48
- fn render_comrak(text: String, options: RenderOptions) -> String {
49
- let mut comrak_options = comrak::ComrakOptions::default();
50
-
51
- comrak_options.extension.alerts = options.alerts;
52
- comrak_options.extension.autolink = options.autolink;
53
- comrak_options.extension.description_lists = options.description_lists;
54
- comrak_options.extension.footnotes = options.footnotes;
55
- // comrak_options.extension.front_matter_delimiter = options.front_matter_delimiter;
56
- comrak_options.extension.greentext = options.greentext;
57
- comrak_options.extension.header_ids = options.header_ids;
58
- comrak_options.extension.math_code = options.math_code;
59
- comrak_options.extension.math_dollars = options.math_dollars;
60
- comrak_options.extension.multiline_block_quotes = options.multiline_block_quotes;
61
- comrak_options.extension.shortcodes = options.gemojis;
62
- comrak_options.extension.spoiler = options.spoiler;
63
- comrak_options.extension.strikethrough = options.strikethrough;
64
- comrak_options.extension.subscript = options.subscript;
65
- comrak_options.extension.superscript = options.superscript;
66
- comrak_options.extension.table = options.table;
67
- comrak_options.extension.tagfilter = options.tagfilter;
68
- comrak_options.extension.tasklist = options.tasklist;
69
- comrak_options.extension.underline = options.underline;
70
- comrak_options.extension.wikilinks_title_after_pipe = options.wikilinks_title_after_pipe;
71
- comrak_options.extension.wikilinks_title_before_pipe = options.wikilinks_title_before_pipe;
72
-
73
- comrak_options.render.escape = options.escape;
74
- comrak_options.render.escaped_char_spans = options.escaped_char_spans;
75
- comrak_options.render.figure_with_caption = options.figure_with_caption;
76
- comrak_options.render.full_info_string = options.full_info_string;
77
- comrak_options.render.gfm_quirks = options.gfm_quirks;
78
- comrak_options.render.github_pre_lang = options.github_pre_lang;
79
- comrak_options.render.hardbreaks = options.hardbreaks;
80
- comrak_options.render.ignore_empty_links = options.ignore_empty_links;
81
- comrak_options.render.ignore_setext = options.ignore_setext;
82
- comrak_options.render.sourcepos = options.sourcepos;
83
- // comrak_options.render.syntax_highlighting = options.syntax_highlighting;
84
-
85
- comrak_options.render.unsafe_ = options.unsafe_;
86
-
87
- // comrak_options.parse.default_info_string = options.default_info_string;
88
- comrak_options.parse.relaxed_autolinks = options.relaxed_autolinks;
89
- comrak_options.parse.relaxed_tasklist_matching = options.relaxed_tasklist_character;
90
- comrak_options.parse.smart = options.smart;
91
-
92
- comrak::markdown_to_html(&text, &comrak_options)
149
+ // The important thing to remember is that this overrides the default behavior of the
150
+ // specified nodes. If we do override a node, then it's our responsibility to ensure that
151
+ // any changes in the `comrak` code for those nodes is backported to here, such as when
152
+ // `figcaption` support was added.
153
+ // One idea to limit that would be having the ability to specify attributes that would
154
+ // be inserted when a node is rendered. That would allow us to (in many cases) just
155
+ // inject the changes we need. Such a feature would need to be added to `comrak`.
156
+ create_formatter!(CustomFormatter<RenderUserData>, {
157
+ NodeValue::Text(_) => |context, node, entering| {
158
+ return render_text(context, node, entering);
159
+ },
160
+ NodeValue::Link(_) => |context, node, entering| {
161
+ return render_link(context, node, entering);
162
+ },
163
+ NodeValue::Image(_) => |context, node, entering| {
164
+ return render_image(context, node, entering);
165
+ }
166
+ });
167
+
168
+ fn render_image<'a>(
169
+ context: &mut Context<RenderUserData>,
170
+ node: &'a AstNode<'a>,
171
+ entering: bool,
172
+ ) -> io::Result<ChildRendering> {
173
+ let NodeValue::Image(ref nl) = node.data.borrow().value else {
174
+ panic!("Attempt to render invalid node as image")
175
+ };
176
+
177
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
178
+ return html::format_node_default(context, node, entering);
179
+ }
180
+
181
+ if entering {
182
+ if context.options.render.figure_with_caption {
183
+ context.write_all(b"<figure>")?;
184
+ }
185
+ context.write_all(b"<img")?;
186
+ html::render_sourcepos(context, node)?;
187
+ context.write_all(b" src=\"")?;
188
+ let url = nl.url.as_bytes();
189
+ if context.options.render.unsafe_ || !html::dangerous_url(url) {
190
+ if let Some(rewriter) = &context.options.extension.image_url_rewriter {
191
+ context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
192
+ } else {
193
+ context.escape_href(url)?;
194
+ }
195
+ }
196
+
197
+ context.write_all(b"\"")?;
198
+
199
+ if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
200
+ context.write_all(b" data-placeholder")?;
201
+ }
202
+
203
+ context.write_all(b" alt=\"")?;
204
+
205
+ return Ok(ChildRendering::Plain);
206
+ } else {
207
+ if !nl.title.is_empty() {
208
+ context.write_all(b"\" title=\"")?;
209
+ context.escape(nl.title.as_bytes())?;
210
+ }
211
+ context.write_all(b"\" />")?;
212
+ if context.options.render.figure_with_caption {
213
+ if !nl.title.is_empty() {
214
+ context.write_all(b"<figcaption>")?;
215
+ context.escape(nl.title.as_bytes())?;
216
+ context.write_all(b"</figcaption>")?;
217
+ }
218
+ context.write_all(b"</figure>")?;
219
+ };
220
+ }
221
+
222
+ Ok(ChildRendering::HTML)
223
+ }
224
+
225
+ fn render_link<'a>(
226
+ context: &mut Context<RenderUserData>,
227
+ node: &'a AstNode<'a>,
228
+ entering: bool,
229
+ ) -> io::Result<ChildRendering> {
230
+ let NodeValue::Link(ref nl) = node.data.borrow().value else {
231
+ panic!("Attempt to render invalid node as link")
232
+ };
233
+
234
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(nl.url.as_str())) {
235
+ return html::format_node_default(context, node, entering);
236
+ }
237
+
238
+ let parent_node = node.parent();
239
+
240
+ if !context.options.parse.relaxed_autolinks
241
+ || (parent_node.is_none()
242
+ || !matches!(
243
+ parent_node.unwrap().data.borrow().value,
244
+ NodeValue::Link(..)
245
+ ))
246
+ {
247
+ if entering {
248
+ context.write_all(b"<a")?;
249
+ html::render_sourcepos(context, node)?;
250
+ context.write_all(b" href=\"")?;
251
+ let url = nl.url.as_bytes();
252
+ if context.options.render.unsafe_ || !html::dangerous_url(url) {
253
+ if let Some(rewriter) = &context.options.extension.link_url_rewriter {
254
+ context.escape_href(rewriter.to_html(&nl.url).as_bytes())?;
255
+ } else {
256
+ context.escape_href(url)?;
257
+ }
258
+ }
259
+ context.write_all(b"\"")?;
260
+
261
+ if !nl.title.is_empty() {
262
+ context.write_all(b" title=\"")?;
263
+ context.escape(nl.title.as_bytes())?;
264
+ }
265
+
266
+ if PLACEHOLDER_REGEX.is_match(nl.url.as_str()) {
267
+ context.write_all(b" data-placeholder")?;
268
+ }
269
+
270
+ context.write_all(b">")?;
271
+ } else {
272
+ context.write_all(b"</a>")?;
273
+ }
274
+ }
275
+
276
+ Ok(ChildRendering::HTML)
277
+ }
278
+
279
+ fn render_text<'a>(
280
+ context: &mut Context<RenderUserData>,
281
+ node: &'a AstNode<'a>,
282
+ entering: bool,
283
+ ) -> io::Result<ChildRendering> {
284
+ let NodeValue::Text(ref literal) = node.data.borrow().value else {
285
+ panic!("Attempt to render invalid node as text")
286
+ };
287
+
288
+ if !(context.user.placeholder_detection && PLACEHOLDER_REGEX.is_match(literal)) {
289
+ return html::format_node_default(context, node, entering);
290
+ }
291
+
292
+ // Don't currently support placeholders in the text inside links or images.
293
+ // If the text has an underscore in it, then the parser will not combine
294
+ // the multiple text nodes in `comrak`'s `postprocess_text_nodes`, breaking up
295
+ // the placeholder into multiple text nodes.
296
+ // For example, `[%{a_b}](link)`.
297
+ let parent = node.parent().unwrap();
298
+ if matches!(
299
+ parent.data.borrow().value,
300
+ NodeValue::Link(_) | NodeValue::Image(_)
301
+ ) {
302
+ return html::format_node_default(context, node, entering);
303
+ }
304
+
305
+ if entering {
306
+ let mut cursor: usize = 0;
307
+
308
+ for mat in PLACEHOLDER_REGEX.find_iter(literal) {
309
+ if mat.start() > cursor {
310
+ context.escape(literal[cursor..mat.start()].as_bytes())?;
311
+ }
312
+
313
+ context.write_all(b"<span data-placeholder>")?;
314
+ context.escape(literal[mat.start()..mat.end()].as_bytes())?;
315
+ context.write_all(b"</span>")?;
316
+
317
+ cursor = mat.end();
318
+ }
319
+
320
+ if cursor < literal.len() {
321
+ context.escape(literal[cursor..literal.len()].as_bytes())?;
322
+ }
323
+ }
324
+
325
+ Ok(ChildRendering::HTML)
93
326
  }
@@ -53,6 +53,8 @@ pub fn render_to_html_rs(text: String, options: RHash) -> String {
53
53
  wikilinks_title_after_pipe: get_bool_opt("wikilinks_title_after_pipe", options),
54
54
  wikilinks_title_before_pipe: get_bool_opt("wikilinks_title_before_pipe", options),
55
55
 
56
+ default_html: get_bool_opt("default_html", options),
57
+ placeholder_detection: get_bool_opt("placeholder_detection", options),
56
58
  debug: get_bool_opt("debug", options),
57
59
  };
58
60
 
@@ -32,6 +32,10 @@ struct Args {
32
32
  #[arg(long)]
33
33
  escaped_char_spans: bool,
34
34
 
35
+ /// Render the image as a figure element with the title as its caption
36
+ #[arg(long)]
37
+ figure_with_caption: bool,
38
+
35
39
  /// Enable 'footnotes' extension
36
40
  #[arg(long)]
37
41
  footnotes: bool,
@@ -158,6 +162,15 @@ struct Args {
158
162
  #[arg(long)]
159
163
  wikilinks_title_before_pipe: bool,
160
164
 
165
+ /// Only use default comrak HTML formatting
166
+ #[arg(long)]
167
+ default_html: bool,
168
+
169
+ /// Detect and marks potential placeholder variables, which
170
+ /// have the format `%{PLACEHOLDER}`
171
+ #[arg(long)]
172
+ placeholder_detection: bool,
173
+
161
174
  /// Show debug information
162
175
  #[arg(long)]
163
176
  debug: bool,
@@ -184,6 +197,7 @@ fn main() {
184
197
  description_lists: cli.description_lists,
185
198
  escape: cli.escape,
186
199
  escaped_char_spans: cli.escaped_char_spans,
200
+ figure_with_caption: cli.figure_with_caption,
187
201
  footnotes: cli.footnotes,
188
202
  // front_matter_delimiter:
189
203
  full_info_string: cli.full_info_string,
@@ -214,6 +228,9 @@ fn main() {
214
228
  unsafe_: cli.unsafe_,
215
229
  wikilinks_title_after_pipe: cli.wikilinks_title_after_pipe,
216
230
  wikilinks_title_before_pipe: cli.wikilinks_title_before_pipe,
231
+
232
+ default_html: cli.default_html,
233
+ placeholder_detection: cli.placeholder_detection,
217
234
  debug: cli.debug,
218
235
  };
219
236
 
Binary file
Binary file
Binary file
Binary file
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GLFMMarkdown
4
- VERSION = '0.0.29'
4
+ VERSION = '0.0.31'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-glfm-markdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.29
4
+ version: 0.0.31
5
5
  platform: aarch64-linux-musl
6
6
  authors:
7
7
  - Brett Walker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-07 00:00:00.000000000 Z
11
+ date: 2025-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -62,6 +62,7 @@ files:
62
62
  - Cargo.lock
63
63
  - LICENSE
64
64
  - README.md
65
+ - ext/glfm_markdown/Cargo.lock
65
66
  - ext/glfm_markdown/Cargo.toml
66
67
  - ext/glfm_markdown/extconf.rb
67
68
  - ext/glfm_markdown/src/glfm.rs