-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.rs
More file actions
297 lines (267 loc) · 10.5 KB
/
Copy pathparser.rs
File metadata and controls
297 lines (267 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! Parser module for Universal Markdown
//!
//! This module provides the core parsing functionality using comrak as the base
//! Markdown parser, with extensions for Universal Markdown-specific syntax.
use comrak::options::{ListStyleType, Plugins};
use comrak::{Arena, Options, format_html_with_plugins, parse_document};
/// Icon markup configuration
///
/// Values can include raw HTML (e.g. `<span class="bi bi-camera-video-fill"></span>`).
/// These values are inserted directly into the HTML output without sanitization —
/// only use trusted, developer-supplied values here.
#[derive(Debug, Clone)]
pub struct Icons {
/// Icon for video fallback download links.
/// Default: `<span class="bi bi-camera-video-fill" aria-hidden="true"></span>`
pub video: String,
/// Icon for audio fallback download links.
/// Default: `<span class="bi bi-music-note-beamed" aria-hidden="true"></span>`
pub audio: String,
/// Icon for downloadable file links.
/// Default: `<span class="bi bi-file-earmark-arrow-down-fill" aria-hidden="true"></span>`
pub download: String,
/// Icon markup rendered inside the inline color swatch.
/// Default: `<span class="bi bi-palette-fill" aria-hidden="true"></span>`
pub color_swatch: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
video: r#"<span class="bi bi-camera-video-fill" aria-hidden="true"></span>"#
.to_string(),
audio: r#"<span class="bi bi-music-note-beamed" aria-hidden="true"></span>"#
.to_string(),
download:
r#"<span class="bi bi-file-earmark-arrow-down-fill" aria-hidden="true"></span>"#
.to_string(),
color_swatch: r#"<span class="bi bi-palette-fill" aria-hidden="true"></span>"#
.to_string(),
}
}
}
/// Parser configuration for Universal Markdown
#[derive(Debug, Clone)]
pub struct ParserOptions {
/// Enable GitHub Flavored Markdown extensions
pub gfm_extensions: bool,
/// Enable Universal Markdown-specific extensions
pub umd_extensions: bool,
/// Maximum heading level (1-5 for Universal Markdown, 1-6 for standard Markdown)
pub max_heading_level: u8,
/// Base URL for resolving absolute paths (e.g., "/umd-core", "https://example.com/app")
/// If set, absolute paths (starting with "/") will be prefixed with this base URL
pub base_url: Option<String>,
/// Allow media type detection from fragment extension hints like `#.png`.
///
/// Disabled by default for safer behavior; enable only when you trust
/// the source content and need extension-less URL fallback.
pub allow_fragment_extension_hint: bool,
/// Maximum allowed nesting depth for inline UMD decoration functions.
///
/// When exceeded, inline decoration expansion is skipped as a fail-safe.
/// Use `None` to disable this limit.
pub max_inline_nesting: Option<u8>,
/// Icon configuration (media fallback links and inline code enhancements)
pub icons: Icons,
}
impl Default for ParserOptions {
fn default() -> Self {
Self {
gfm_extensions: true,
umd_extensions: true,
max_heading_level: 5,
base_url: None,
allow_fragment_extension_hint: false,
max_inline_nesting: Some(5),
icons: Icons::default(),
}
}
}
/// Parse Universal Markdown and convert to HTML
///
/// # Arguments
///
/// * `input` - The sanitized Universal Markdown source text
/// * `options` - Parser configuration options
///
/// # Returns
///
/// HTML string
///
/// # Examples
///
/// ```
/// use umd::parser::{parse_to_html, ParserOptions};
///
/// let input = "# Hello World\n\nThis is **bold** text.";
/// let html = parse_to_html(input, &ParserOptions::default());
/// assert!(html.contains("<h1>"));
/// assert!(html.contains("<strong>"));
/// ```
pub fn parse_to_html(input: &str, options: &ParserOptions) -> String {
// Configure comrak options
let mut comrak_options = Options::default();
// Enable extensions
if options.gfm_extensions {
comrak_options.extension.strikethrough = true;
comrak_options.extension.tagfilter = true; // Disallow dangerous HTML tags
comrak_options.extension.table = true;
comrak_options.extension.autolink = true;
comrak_options.extension.tasklist = true;
comrak_options.extension.footnotes = true; // Enable footnotes
comrak_options.extension.header_id_prefix = None; // Disable automatic IDs, we'll add them ourselves
}
// Render options
comrak_options.render.hardbreaks = false;
comrak_options.render.github_pre_lang = false; // Keep language on <code class="language-*"> (no lang on <pre>)
comrak_options.render.full_info_string = true;
comrak_options.render.width = 0;
comrak_options.render.r#unsafe = false; // Don't render raw HTML
comrak_options.render.escape = false;
comrak_options.render.list_style = ListStyleType::Dash;
// Create arena for AST nodes
let arena = Arena::new();
// Parse markdown to AST
let root = parse_document(&arena, input, &comrak_options);
// Render to HTML
let mut html = String::new();
format_html_with_plugins(root, &comrak_options, &mut html, &Plugins::default())
.expect("Failed to render HTML");
html
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_heading() {
let input = "# Heading 1\n## Heading 2";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<h1>"));
assert!(html.contains("Heading 1"));
assert!(html.contains("<h2>"));
assert!(html.contains("Heading 2"));
}
#[test]
fn test_paragraph() {
let input = "This is a paragraph.";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<p>"));
assert!(html.contains("This is a paragraph."));
}
#[test]
fn test_bold_italic() {
let input = "**bold** and *italic*";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<strong>bold</strong>"));
assert!(html.contains("<em>italic</em>"));
}
#[test]
fn test_unordered_list() {
let input = "- Item 1\n- Item 2\n- Item 3";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<ul>"));
assert!(html.contains("<li>Item 1</li>"));
assert!(html.contains("<li>Item 2</li>"));
}
#[test]
fn test_ordered_list() {
let input = "1. First\n2. Second\n3. Third";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<ol>"));
assert!(html.contains("<li>First</li>"));
assert!(html.contains("<li>Second</li>"));
}
#[test]
fn test_code_block() {
let input = "```rust\nfn main() {}\n```";
let html = parse_to_html(input, &ParserOptions::default());
println!("HTML output: {}", html);
// comrak wraps code blocks in <pre><code> tags
assert!(html.contains("<code") || html.contains("fn main() {}"));
assert!(html.contains("rust") || html.contains("language-rust"));
}
#[test]
fn test_inline_code() {
let input = "This is `inline code` example.";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<code>inline code</code>"));
}
#[test]
fn test_link() {
let input = "[Link text](https://example.com)";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<a href=\"https://example.com\">"));
assert!(html.contains("Link text"));
}
#[test]
fn test_image() {
let input = "";
let html = crate::parse(input);
// Now images are wrapped in <picture> tags
assert!(html.contains("<picture"));
assert!(html.contains("src=\"https://example.com/image.png\""));
assert!(html.contains("alt=\"Alt text\""));
}
#[test]
fn test_gfm_strikethrough() {
let input = "~~strikethrough~~";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<del>strikethrough</del>"));
}
#[test]
fn test_gfm_table() {
let input = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("<table>"));
assert!(html.contains("<th>Header 1</th>"));
assert!(html.contains("<td>Cell 1</td>"));
}
#[test]
fn test_task_list() {
let input = "- [ ] Unchecked\n- [x] Checked";
let html = parse_to_html(input, &ParserOptions::default());
assert!(html.contains("type=\"checkbox\""));
assert!(html.contains("disabled"));
}
#[test]
fn test_video_media() {
let input = "";
let html = crate::parse(input);
println!("Video HTML output: {}", html);
assert!(html.contains("<video controls"));
assert!(html.contains("src=\"https://example.com/video.mp4\""));
assert!(html.contains("type=\"video/mp4\""));
assert!(html.contains("<track kind=\"captions\" label=\"Demo video\""));
}
#[test]
fn test_audio_media() {
let input = "";
let html = crate::parse(input);
assert!(html.contains("<audio controls"));
assert!(html.contains("src=\"https://example.com/audio.mp3\""));
assert!(html.contains("type=\"audio/mpeg\""));
}
#[test]
fn test_image_with_title() {
let input = "";
let html = crate::parse(input);
assert!(html.contains("<picture"));
assert!(html.contains("title=\"Company Logo\""));
assert!(html.contains("alt=\"Logo\""));
}
#[test]
fn test_video_with_title() {
let input = "";
let html = crate::parse(input);
assert!(html.contains("<video controls"));
assert!(html.contains("title=\"Our new product\""));
}
#[test]
fn test_jxl_image() {
let input = "";
let html = crate::parse(input);
assert!(html.contains("<picture"));
assert!(html.contains("type=\"image/jxl\""));
assert!(html.contains("title=\"JPEG XL format\""));
}
}