socketry_markdown/configuration.rs
1// Released under the MIT License.
2// Copyright, 2022, by Bernhard Berger.
3// Copyright, 2022-2025, by Titus Wormer.
4// Copyright, 2023, by cel.
5// Copyright, 2025, by Ophir Lojkine.
6// Copyright, 2026, by Samuel Williams.
7
8use crate::util::{
9 line_ending::LineEnding,
10 mdx::{EsmParse as MdxEsmParse, ExpressionParse as MdxExpressionParse},
11};
12use alloc::{boxed::Box, fmt, string::String};
13
14/// Control which constructs are enabled.
15///
16/// Not all constructs can be configured.
17/// Notably, blank lines and paragraphs cannot be turned off.
18///
19/// ## Examples
20///
21/// ```
22/// use socketry_markdown::Constructs;
23/// # fn main() {
24///
25/// // Use the default trait to get `CommonMark` constructs:
26/// let commonmark = Constructs::default();
27///
28/// // To turn on all of GFM, use the `gfm` method:
29/// let gfm = Constructs::gfm();
30///
31/// // Or, mix and match:
32/// let custom = Constructs {
33/// math_flow: true,
34/// math_text: true,
35/// ..Constructs::gfm()
36/// };
37/// # }
38/// ```
39#[allow(clippy::struct_excessive_bools)]
40#[derive(Clone, Debug, Eq, PartialEq)]
41#[cfg_attr(
42 feature = "serde",
43 derive(serde::Serialize, serde::Deserialize),
44 serde(rename_all = "camelCase")
45)]
46pub struct Constructs {
47 /// Attention.
48 ///
49 /// ```markdown
50 /// > | a *b* c **d**.
51 /// ^^^ ^^^^^
52 /// ```
53 pub attention: bool,
54 /// Autolink.
55 ///
56 /// ```markdown
57 /// > | a <https://example.com> b <user@example.org>.
58 /// ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
59 /// ```
60 pub autolink: bool,
61 /// Block quote.
62 ///
63 /// ```markdown
64 /// > | > a
65 /// ^^^
66 /// ```
67 pub block_quote: bool,
68 /// Character escape.
69 ///
70 /// ```markdown
71 /// > | a \* b
72 /// ^^
73 /// ```
74 pub character_escape: bool,
75 /// Character reference.
76 ///
77 /// ```markdown
78 /// > | a & b
79 /// ^^^^^
80 /// ```
81 pub character_reference: bool,
82 /// Code (indented).
83 ///
84 /// ```markdown
85 /// > | a
86 /// ^^^^^
87 /// ```
88 pub code_indented: bool,
89 /// Code (fenced).
90 ///
91 /// ```markdown
92 /// > | ~~~js
93 /// ^^^^^
94 /// > | console.log(1)
95 /// ^^^^^^^^^^^^^^
96 /// > | ~~~
97 /// ^^^
98 /// ```
99 pub code_fenced: bool,
100 /// Code (text).
101 ///
102 /// ```markdown
103 /// > | a `b` c
104 /// ^^^
105 /// ```
106 pub code_text: bool,
107 /// Definition.
108 ///
109 /// ```markdown
110 /// > | [a]: b "c"
111 /// ^^^^^^^^^^
112 /// ```
113 pub definition: bool,
114 /// Frontmatter.
115 ///
116 /// ````markdown
117 /// > | ---
118 /// ^^^
119 /// > | title: Neptune
120 /// ^^^^^^^^^^^^^^
121 /// > | ---
122 /// ^^^
123 /// ````
124 pub frontmatter: bool,
125 /// GFM: autolink literal.
126 ///
127 /// ```markdown
128 /// > | https://example.com
129 /// ^^^^^^^^^^^^^^^^^^^
130 /// ```
131 pub gfm_autolink_literal: bool,
132 /// GFM: footnote definition.
133 ///
134 /// ```markdown
135 /// > | [^a]: b
136 /// ^^^^^^^
137 /// ```
138 pub gfm_footnote_definition: bool,
139 /// GFM: footnote label start.
140 ///
141 /// ```markdown
142 /// > | a[^b]
143 /// ^^
144 /// ```
145 pub gfm_label_start_footnote: bool,
146 ///
147 /// ```markdown
148 /// > | a ~b~ c.
149 /// ^^^
150 /// ```
151 pub gfm_strikethrough: bool,
152 /// GFM: table.
153 ///
154 /// ```markdown
155 /// > | | a |
156 /// ^^^^^
157 /// > | | - |
158 /// ^^^^^
159 /// > | | b |
160 /// ^^^^^
161 /// ```
162 pub gfm_table: bool,
163 /// GFM: task list item.
164 ///
165 /// ```markdown
166 /// > | * [x] y.
167 /// ^^^
168 /// ```
169 pub gfm_task_list_item: bool,
170 /// Hard break (escape).
171 ///
172 /// ```markdown
173 /// > | a\
174 /// ^
175 /// | b
176 /// ```
177 pub hard_break_escape: bool,
178 /// Hard break (trailing).
179 ///
180 /// ```markdown
181 /// > | a␠␠
182 /// ^^
183 /// | b
184 /// ```
185 pub hard_break_trailing: bool,
186 /// Heading (atx).
187 ///
188 /// ```markdown
189 /// > | # a
190 /// ^^^
191 /// ```
192 pub heading_atx: bool,
193 /// Heading (setext).
194 ///
195 /// ```markdown
196 /// > | a
197 /// ^^
198 /// > | ==
199 /// ^^
200 /// ```
201 pub heading_setext: bool,
202 /// HTML (flow).
203 ///
204 /// ```markdown
205 /// > | <div>
206 /// ^^^^^
207 /// ```
208 pub html_flow: bool,
209 /// HTML (text).
210 ///
211 /// ```markdown
212 /// > | a <b> c
213 /// ^^^
214 /// ```
215 pub html_text: bool,
216 /// Label start (image).
217 ///
218 /// ```markdown
219 /// > | a  d
220 /// ^^
221 /// ```
222 pub label_start_image: bool,
223 /// Label start (link).
224 ///
225 /// ```markdown
226 /// > | a [b](c) d
227 /// ^
228 /// ```
229 pub label_start_link: bool,
230 /// Label end.
231 ///
232 /// ```markdown
233 /// > | a [b](c) d
234 /// ^^^^
235 /// ```
236 pub label_end: bool,
237 /// List items.
238 ///
239 /// ```markdown
240 /// > | * a
241 /// ^^^
242 /// ```
243 pub list_item: bool,
244 /// Math (flow).
245 ///
246 /// ```markdown
247 /// > | $$
248 /// ^^
249 /// > | \frac{1}{2}
250 /// ^^^^^^^^^^^
251 /// > | $$
252 /// ^^
253 /// ```
254 pub math_flow: bool,
255 /// Math (text).
256 ///
257 /// ```markdown
258 /// > | a $b$ c
259 /// ^^^
260 /// ```
261 pub math_text: bool,
262 /// MDX: ESM.
263 ///
264 /// ```markdown
265 /// > | import a from 'b'
266 /// ^^^^^^^^^^^^^^^^^
267 /// ```
268 ///
269 /// > 👉 **Note**: to support ESM, you *must* pass
270 /// > [`mdx_esm_parse`][MdxEsmParse] in [`ParseOptions`][] too.
271 /// > Otherwise, ESM is treated as normal markdown.
272 pub mdx_esm: bool,
273 /// MDX: expression (flow).
274 ///
275 /// ```markdown
276 /// > | {Math.PI}
277 /// ^^^^^^^^^
278 /// ```
279 ///
280 /// > 👉 **Note**: You *can* pass
281 /// > [`mdx_expression_parse`][MdxExpressionParse] in [`ParseOptions`][]
282 /// > too, to parse expressions according to a certain grammar (typically,
283 /// > a programming language).
284 /// > Otherwise, expressions are parsed with a basic algorithm that only
285 /// > cares about braces.
286 pub mdx_expression_flow: bool,
287 /// MDX: expression (text).
288 ///
289 /// ```markdown
290 /// > | a {Math.PI} c
291 /// ^^^^^^^^^
292 /// ```
293 ///
294 /// > 👉 **Note**: You *can* pass
295 /// > [`mdx_expression_parse`][MdxExpressionParse] in [`ParseOptions`][]
296 /// > too, to parse expressions according to a certain grammar (typically,
297 /// > a programming language).
298 /// > Otherwise, expressions are parsed with a basic algorithm that only
299 /// > cares about braces.
300 pub mdx_expression_text: bool,
301 /// MDX: JSX (flow).
302 ///
303 /// ```markdown
304 /// > | <Component />
305 /// ^^^^^^^^^^^^^
306 /// ```
307 ///
308 /// > 👉 **Note**: You *must* pass `html_flow: false` to use this,
309 /// > as it’s preferred when on over `mdx_jsx_flow`.
310 ///
311 /// > 👉 **Note**: You *can* pass
312 /// > [`mdx_expression_parse`][MdxExpressionParse] in [`ParseOptions`][]
313 /// > too, to parse expressions in JSX according to a certain grammar
314 /// > (typically, a programming language).
315 /// > Otherwise, expressions are parsed with a basic algorithm that only
316 /// > cares about braces.
317 pub mdx_jsx_flow: bool,
318 /// MDX: JSX (text).
319 ///
320 /// ```markdown
321 /// > | a <Component /> c
322 /// ^^^^^^^^^^^^^
323 /// ```
324 ///
325 /// > 👉 **Note**: You *must* pass `html_text: false` to use this,
326 /// > as it’s preferred when on over `mdx_jsx_text`.
327 ///
328 /// > 👉 **Note**: You *can* pass
329 /// > [`mdx_expression_parse`][MdxExpressionParse] in [`ParseOptions`][]
330 /// > too, to parse expressions in JSX according to a certain grammar
331 /// > (typically, a programming language).
332 /// > Otherwise, expressions are parsed with a basic algorithm that only
333 /// > cares about braces.
334 pub mdx_jsx_text: bool,
335 /// Thematic break.
336 ///
337 /// ```markdown
338 /// > | ***
339 /// ^^^
340 /// ```
341 pub thematic_break: bool,
342}
343
344impl Default for Constructs {
345 /// `CommonMark`.
346 ///
347 /// `CommonMark` is a relatively strong specification of how markdown
348 /// works.
349 /// Most markdown parsers try to follow it.
350 ///
351 /// For more information, see the `CommonMark` specification:
352 /// <https://spec.commonmark.org>.
353 fn default() -> Self {
354 Self {
355 attention: true,
356 autolink: true,
357 block_quote: true,
358 character_escape: true,
359 character_reference: true,
360 code_indented: true,
361 code_fenced: true,
362 code_text: true,
363 definition: true,
364 frontmatter: false,
365 gfm_autolink_literal: false,
366 gfm_label_start_footnote: false,
367 gfm_footnote_definition: false,
368 gfm_strikethrough: false,
369 gfm_table: false,
370 gfm_task_list_item: false,
371 hard_break_escape: true,
372 hard_break_trailing: true,
373 heading_atx: true,
374 heading_setext: true,
375 html_flow: true,
376 html_text: true,
377 label_start_image: true,
378 label_start_link: true,
379 label_end: true,
380 list_item: true,
381 math_flow: false,
382 math_text: false,
383 mdx_esm: false,
384 mdx_expression_flow: false,
385 mdx_expression_text: false,
386 mdx_jsx_flow: false,
387 mdx_jsx_text: false,
388 thematic_break: true,
389 }
390 }
391}
392
393impl Constructs {
394 /// GFM.
395 ///
396 /// GFM stands for **GitHub flavored markdown**.
397 /// GFM extends `CommonMark` and adds support for autolink literals,
398 /// footnotes, strikethrough, tables, and tasklists.
399 ///
400 /// For more information, see the GFM specification:
401 /// <https://github.github.com/gfm/>.
402 pub fn gfm() -> Self {
403 Self {
404 gfm_autolink_literal: true,
405 gfm_footnote_definition: true,
406 gfm_label_start_footnote: true,
407 gfm_strikethrough: true,
408 gfm_table: true,
409 gfm_task_list_item: true,
410 ..Self::default()
411 }
412 }
413
414 /// MDX.
415 ///
416 /// This turns on `CommonMark`, turns off some conflicting constructs
417 /// (autolinks, code (indented), and HTML), and turns on MDX (ESM,
418 /// expressions, and JSX).
419 ///
420 /// For more information, see the MDX website:
421 /// <https://mdxjs.com>.
422 ///
423 /// > 👉 **Note**: to support ESM, you *must* pass
424 /// > [`mdx_esm_parse`][MdxEsmParse] in [`ParseOptions`][] too.
425 /// > Otherwise, ESM is treated as normal markdown.
426 /// >
427 /// > You *can* pass
428 /// > [`mdx_expression_parse`][MdxExpressionParse]
429 /// > to parse expressions according to a certain grammar (typically, a
430 /// > programming language).
431 /// > Otherwise, expressions are parsed with a basic algorithm that only
432 /// > cares about braces.
433 pub fn mdx() -> Self {
434 Self {
435 autolink: false,
436 code_indented: false,
437 html_flow: false,
438 html_text: false,
439 mdx_esm: true,
440 mdx_expression_flow: true,
441 mdx_expression_text: true,
442 mdx_jsx_flow: true,
443 mdx_jsx_text: true,
444 ..Self::default()
445 }
446 }
447}
448
449/// Configuration that describes how to compile to HTML.
450///
451/// You likely either want to turn on the dangerous options
452/// (`allow_dangerous_html`, `allow_dangerous_protocol`) when dealing with
453/// input you trust, or want to customize how GFM footnotes are compiled
454/// (typically because the input markdown is not in English).
455///
456/// ## Examples
457///
458/// ```
459/// use socketry_markdown::CompileOptions;
460/// # fn main() {
461///
462/// // Use the default trait to get safe defaults:
463/// let safe = CompileOptions::default();
464///
465/// // Live dangerously / trust the author:
466/// let danger = CompileOptions {
467/// allow_dangerous_html: true,
468/// allow_dangerous_protocol: true,
469/// ..CompileOptions::default()
470/// };
471///
472/// // In French:
473/// let enFrançais = CompileOptions {
474/// gfm_footnote_back_label: Some("Arrière".into()),
475/// gfm_footnote_label: Some("Notes de bas de page".into()),
476/// ..CompileOptions::default()
477/// };
478/// # }
479/// ```
480#[allow(clippy::struct_excessive_bools)]
481#[derive(Clone, Debug, Default)]
482#[cfg_attr(
483 feature = "serde",
484 derive(serde::Serialize, serde::Deserialize),
485 serde(default, rename_all = "camelCase")
486)]
487pub struct CompileOptions {
488 /// Whether to allow all values in images.
489 ///
490 /// The default is `false`,
491 /// which lets `allow_dangerous_protocol` control protocol safety for
492 /// both links and images.
493 ///
494 /// Pass `true` to allow all values as `src` on images,
495 /// regardless of `allow_dangerous_protocol`.
496 /// This is safe because the
497 /// [HTML specification][whatwg-html-image-processing]
498 /// does not allow executable code in images.
499 ///
500 /// [whatwg-html-image-processing]: https://html.spec.whatwg.org/multipage/images.html#images-processing-model
501 ///
502 /// ## Examples
503 ///
504 /// ```
505 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options};
506 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
507 ///
508 /// // By default, some protocols in image sources are dropped:
509 /// assert_eq!(
510 /// to_html_with_options(
511 /// "",
512 /// &Options::default()
513 /// )?,
514 /// "<p><img src=\"\" alt=\"\" /></p>"
515 /// );
516 ///
517 /// // Turn `allow_any_img_src` on to allow all values as `src` on images.
518 /// // This is safe because browsers do not execute code in images.
519 /// assert_eq!(
520 /// to_html_with_options(
521 /// ")",
522 /// &Options {
523 /// compile: CompileOptions {
524 /// allow_any_img_src: true,
525 /// ..CompileOptions::default()
526 /// },
527 /// ..Options::default()
528 /// }
529 /// )?,
530 /// "<p><img src=\"javascript:alert(1)\" alt=\"\" /></p>"
531 /// );
532 /// # Ok(())
533 /// # }
534 /// ```
535 pub allow_any_img_src: bool,
536
537 /// Whether to allow (dangerous) HTML.
538 ///
539 /// The default is `false`, which still parses the HTML according to
540 /// `CommonMark` but shows the HTML as text instead of as elements.
541 ///
542 /// Pass `true` for trusted content to get actual HTML elements.
543 ///
544 /// When using GFM, make sure to also turn off `gfm_tagfilter`.
545 /// Otherwise, some dangerous HTML is still ignored.
546 ///
547 /// ## Examples
548 ///
549 /// ```
550 /// use socketry_markdown::{to_html, to_html_with_options, CompileOptions, Options};
551 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
552 ///
553 /// // `markdown-rs` is safe by default:
554 /// assert_eq!(
555 /// to_html("Hi, <i>venus</i>!"),
556 /// "<p>Hi, <i>venus</i>!</p>"
557 /// );
558 ///
559 /// // Turn `allow_dangerous_html` on to allow potentially dangerous HTML:
560 /// assert_eq!(
561 /// to_html_with_options(
562 /// "Hi, <i>venus</i>!",
563 /// &Options {
564 /// compile: CompileOptions {
565 /// allow_dangerous_html: true,
566 /// ..CompileOptions::default()
567 /// },
568 /// ..Options::default()
569 /// }
570 /// )?,
571 /// "<p>Hi, <i>venus</i>!</p>"
572 /// );
573 /// # Ok(())
574 /// # }
575 /// ```
576 pub allow_dangerous_html: bool,
577
578 /// Whether to allow dangerous protocols in links and images.
579 ///
580 /// The default is `false`, which drops URLs in links and images that use
581 /// dangerous protocols.
582 ///
583 /// Pass `true` for trusted content to support all protocols.
584 ///
585 /// URLs that have no protocol (which means it’s relative to the current
586 /// page, such as `./some/page.html`) and URLs that have a safe protocol
587 /// (for images: `http`, `https`; for links: `http`, `https`, `irc`,
588 /// `ircs`, `mailto`, `xmpp`), are safe.
589 /// All other URLs are dangerous and dropped.
590 ///
591 /// When the option `allow_all_protocols_in_img` is enabled,
592 /// `allow_dangerous_protocol` only applies to links.
593 ///
594 /// This is safe because the
595 /// [HTML specification][whatwg-html-image-processing]
596 /// does not allow executable code in images.
597 /// All modern browsers respect this.
598 ///
599 /// [whatwg-html-image-processing]: https://html.spec.whatwg.org/multipage/images.html#images-processing-model
600 ///
601 /// ## Examples
602 ///
603 /// ```
604 /// use socketry_markdown::{to_html, to_html_with_options, CompileOptions, Options};
605 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
606 ///
607 /// // `markdown-rs` is safe by default:
608 /// assert_eq!(
609 /// to_html("<javascript:alert(1)>"),
610 /// "<p><a href=\"\">javascript:alert(1)</a></p>"
611 /// );
612 ///
613 /// // Turn `allow_dangerous_protocol` on to allow potentially dangerous protocols:
614 /// assert_eq!(
615 /// to_html_with_options(
616 /// "<javascript:alert(1)>",
617 /// &Options {
618 /// compile: CompileOptions {
619 /// allow_dangerous_protocol: true,
620 /// ..CompileOptions::default()
621 /// },
622 /// ..Options::default()
623 /// }
624 /// )?,
625 /// "<p><a href=\"javascript:alert(1)\">javascript:alert(1)</a></p>"
626 /// );
627 /// # Ok(())
628 /// # }
629 /// ```
630 pub allow_dangerous_protocol: bool,
631
632 // To do: `doc_markdown` is broken.
633 #[allow(clippy::doc_markdown)]
634 /// Default line ending to use when compiling to HTML, for line endings not
635 /// in `value`.
636 ///
637 /// Generally, `markdown-rs` copies line endings (`\r`, `\n`, `\r\n`) in
638 /// the markdown document over to the compiled HTML.
639 /// In some cases, such as `> a`, CommonMark requires that extra line
640 /// endings are added: `<blockquote>\n<p>a</p>\n</blockquote>`.
641 ///
642 /// To create that line ending, the document is checked for the first line
643 /// ending that is used.
644 /// If there is no line ending, `default_line_ending` is used.
645 /// If that isn’t configured, `\n` is used.
646 ///
647 /// ## Examples
648 ///
649 /// ```
650 /// use socketry_markdown::{to_html, to_html_with_options, CompileOptions, LineEnding, Options};
651 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
652 ///
653 /// // `markdown-rs` uses `\n` by default:
654 /// assert_eq!(
655 /// to_html("> a"),
656 /// "<blockquote>\n<p>a</p>\n</blockquote>"
657 /// );
658 ///
659 /// // Define `default_line_ending` to configure the default:
660 /// assert_eq!(
661 /// to_html_with_options(
662 /// "> a",
663 /// &Options {
664 /// compile: CompileOptions {
665 /// default_line_ending: LineEnding::CarriageReturnLineFeed,
666 /// ..CompileOptions::default()
667 /// },
668 /// ..Options::default()
669 /// }
670 /// )?,
671 /// "<blockquote>\r\n<p>a</p>\r\n</blockquote>"
672 /// );
673 /// # Ok(())
674 /// # }
675 /// ```
676 pub default_line_ending: LineEnding,
677
678 /// Textual label to describe the backreference back to footnote calls.
679 ///
680 /// The default value is `"Back to content"`.
681 /// Change it when the markdown is not in English.
682 ///
683 /// This label is used in the `aria-label` attribute on each backreference
684 /// (the `↩` links).
685 /// It affects users of assistive technology.
686 ///
687 /// ## Examples
688 ///
689 /// ```
690 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
691 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
692 ///
693 /// // `"Back to content"` is used by default:
694 /// assert_eq!(
695 /// to_html_with_options(
696 /// "[^a]\n\n[^a]: b",
697 /// &Options::gfm()
698 /// )?,
699 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
700 /// );
701 ///
702 /// // Pass `gfm_footnote_back_label` to use something else:
703 /// assert_eq!(
704 /// to_html_with_options(
705 /// "[^a]\n\n[^a]: b",
706 /// &Options {
707 /// parse: ParseOptions::gfm(),
708 /// compile: CompileOptions {
709 /// gfm_footnote_back_label: Some("Arrière".into()),
710 /// ..CompileOptions::gfm()
711 /// }
712 /// }
713 /// )?,
714 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Arrière\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
715 /// );
716 /// # Ok(())
717 /// # }
718 /// ```
719 pub gfm_footnote_back_label: Option<String>,
720
721 /// Prefix to use before the `id` attribute on footnotes to prevent them
722 /// from *clobbering*.
723 ///
724 /// The default is `"user-content-"`.
725 /// Pass `Some("".into())` for trusted markdown and when you are careful
726 /// with polyfilling.
727 /// You could pass a different prefix.
728 ///
729 /// DOM clobbering is this:
730 ///
731 /// ```html
732 /// <p id="x"></p>
733 /// <script>alert(x) // `x` now refers to the `p#x` DOM element</script>
734 /// ```
735 ///
736 /// The above example shows that elements are made available by browsers,
737 /// by their ID, on the `window` object.
738 /// This is a security risk because you might be expecting some other
739 /// variable at that place.
740 /// It can also break polyfills.
741 /// Using a prefix solves these problems.
742 ///
743 /// ## Examples
744 ///
745 /// ```
746 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
747 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
748 ///
749 /// // `"user-content-"` is used by default:
750 /// assert_eq!(
751 /// to_html_with_options(
752 /// "[^a]\n\n[^a]: b",
753 /// &Options::gfm()
754 /// )?,
755 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
756 /// );
757 ///
758 /// // Pass `gfm_footnote_clobber_prefix` to use something else:
759 /// assert_eq!(
760 /// to_html_with_options(
761 /// "[^a]\n\n[^a]: b",
762 /// &Options {
763 /// parse: ParseOptions::gfm(),
764 /// compile: CompileOptions {
765 /// gfm_footnote_clobber_prefix: Some("".into()),
766 /// ..CompileOptions::gfm()
767 /// }
768 /// }
769 /// )?,
770 /// "<p><sup><a href=\"#fn-a\" id=\"fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"fn-a\">\n<p>b <a href=\"#fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
771 /// );
772 /// # Ok(())
773 /// # }
774 /// ```
775 pub gfm_footnote_clobber_prefix: Option<String>,
776
777 /// Attributes to use on the footnote label.
778 ///
779 /// The default value is `"class=\"sr-only\""`.
780 /// Change it to show the label and add other attributes.
781 ///
782 /// This label is typically hidden visually (assuming a `sr-only` CSS class
783 /// is defined that does that), and thus affects screen readers only.
784 /// If you do have such a class, but want to show this section to everyone,
785 /// pass an empty string.
786 /// You can also add different attributes.
787 ///
788 /// > 👉 **Note**: `id="footnote-label"` is always added, because footnote
789 /// > calls use it with `aria-describedby` to provide an accessible label.
790 ///
791 /// ## Examples
792 ///
793 /// ```
794 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
795 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
796 ///
797 /// // `"class=\"sr-only\""` is used by default:
798 /// assert_eq!(
799 /// to_html_with_options(
800 /// "[^a]\n\n[^a]: b",
801 /// &Options::gfm()
802 /// )?,
803 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
804 /// );
805 ///
806 /// // Pass `gfm_footnote_label_attributes` to use something else:
807 /// assert_eq!(
808 /// to_html_with_options(
809 /// "[^a]\n\n[^a]: b",
810 /// &Options {
811 /// parse: ParseOptions::gfm(),
812 /// compile: CompileOptions {
813 /// gfm_footnote_label_attributes: Some("class=\"footnote-heading\"".into()),
814 /// ..CompileOptions::gfm()
815 /// }
816 /// }
817 /// )?,
818 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"footnote-heading\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
819 /// );
820 /// # Ok(())
821 /// # }
822 /// ```
823 pub gfm_footnote_label_attributes: Option<String>,
824
825 /// HTML tag name to use for the footnote label element.
826 ///
827 /// The default value is `"h2"`.
828 /// Change it to match your document structure.
829 ///
830 /// This label is typically hidden visually (assuming a `sr-only` CSS class
831 /// is defined that does that), and thus affects screen readers only.
832 /// If you do have such a class, but want to show this section to everyone,
833 /// pass different attributes with the `gfm_footnote_label_attributes`
834 /// option.
835 ///
836 /// ## Examples
837 ///
838 /// ```
839 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
840 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
841 ///
842 /// // `"h2"` is used by default:
843 /// assert_eq!(
844 /// to_html_with_options(
845 /// "[^a]\n\n[^a]: b",
846 /// &Options::gfm()
847 /// )?,
848 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
849 /// );
850 ///
851 /// // Pass `gfm_footnote_label_tag_name` to use something else:
852 /// assert_eq!(
853 /// to_html_with_options(
854 /// "[^a]\n\n[^a]: b",
855 /// &Options {
856 /// parse: ParseOptions::gfm(),
857 /// compile: CompileOptions {
858 /// gfm_footnote_label_tag_name: Some("h1".into()),
859 /// ..CompileOptions::gfm()
860 /// }
861 /// }
862 /// )?,
863 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h1 id=\"footnote-label\" class=\"sr-only\">Footnotes</h1>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
864 /// );
865 /// # Ok(())
866 /// # }
867 /// ```
868 pub gfm_footnote_label_tag_name: Option<String>,
869
870 /// Textual label to use for the footnotes section.
871 ///
872 /// The default value is `"Footnotes"`.
873 /// Change it when the markdown is not in English.
874 ///
875 /// This label is typically hidden visually (assuming a `sr-only` CSS class
876 /// is defined that does that), and thus affects screen readers only.
877 /// If you do have such a class, but want to show this section to everyone,
878 /// pass different attributes with the `gfm_footnote_label_attributes`
879 /// option.
880 ///
881 /// ## Examples
882 ///
883 /// ```
884 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
885 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
886 ///
887 /// // `"Footnotes"` is used by default:
888 /// assert_eq!(
889 /// to_html_with_options(
890 /// "[^a]\n\n[^a]: b",
891 /// &Options::gfm()
892 /// )?,
893 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
894 /// );
895 ///
896 /// // Pass `gfm_footnote_label` to use something else:
897 /// assert_eq!(
898 /// to_html_with_options(
899 /// "[^a]\n\n[^a]: b",
900 /// &Options {
901 /// parse: ParseOptions::gfm(),
902 /// compile: CompileOptions {
903 /// gfm_footnote_label: Some("Notes de bas de page".into()),
904 /// ..CompileOptions::gfm()
905 /// }
906 /// }
907 /// )?,
908 /// "<p><sup><a href=\"#user-content-fn-a\" id=\"user-content-fnref-a\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">1</a></sup></p>\n<section data-footnotes=\"\" class=\"footnotes\"><h2 id=\"footnote-label\" class=\"sr-only\">Notes de bas de page</h2>\n<ol>\n<li id=\"user-content-fn-a\">\n<p>b <a href=\"#user-content-fnref-a\" data-footnote-backref=\"\" aria-label=\"Back to content\" class=\"data-footnote-backref\">↩</a></p>\n</li>\n</ol>\n</section>\n"
909 /// );
910 /// # Ok(())
911 /// # }
912 /// ```
913 pub gfm_footnote_label: Option<String>,
914
915 /// Whether or not GFM task list html `<input>` items are enabled.
916 ///
917 /// This determines whether or not the user of the browser is able
918 /// to click and toggle generated checkbox items. The default is false.
919 ///
920 /// ## Examples
921 ///
922 /// ```
923 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
924 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
925 ///
926 /// // With `gfm_task_list_item_checkable`, generated `<input type="checkbox" />`
927 /// // tags do not contain the attribute `disabled=""` and are thus toggleable by
928 /// // browser users.
929 /// assert_eq!(
930 /// to_html_with_options(
931 /// "* [x] y.",
932 /// &Options {
933 /// parse: ParseOptions::gfm(),
934 /// compile: CompileOptions {
935 /// gfm_task_list_item_checkable: true,
936 /// ..CompileOptions::gfm()
937 /// }
938 /// }
939 /// )?,
940 /// "<ul>\n<li><input type=\"checkbox\" checked=\"\" /> y.</li>\n</ul>"
941 /// );
942 /// # Ok(())
943 /// # }
944 /// ```
945 pub gfm_task_list_item_checkable: bool,
946
947 /// Whether to support the GFM tagfilter.
948 ///
949 /// This option does nothing if `allow_dangerous_html` is not turned on.
950 /// The default is `false`, which does not apply the GFM tagfilter to HTML.
951 /// Pass `true` for output that is a bit closer to GitHub’s actual output.
952 ///
953 /// The tagfilter is kinda weird and kinda useless.
954 /// The tag filter is a naïve attempt at XSS protection.
955 /// You should use a proper HTML sanitizing algorithm instead.
956 ///
957 /// ## Examples
958 ///
959 /// ```
960 /// use socketry_markdown::{to_html_with_options, CompileOptions, Options, ParseOptions};
961 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
962 ///
963 /// // With `allow_dangerous_html`, `markdown-rs` passes HTML through untouched:
964 /// assert_eq!(
965 /// to_html_with_options(
966 /// "<iframe>",
967 /// &Options {
968 /// parse: ParseOptions::gfm(),
969 /// compile: CompileOptions {
970 /// allow_dangerous_html: true,
971 /// ..CompileOptions::default()
972 /// }
973 /// }
974 /// )?,
975 /// "<iframe>"
976 /// );
977 ///
978 /// // Pass `gfm_tagfilter: true` to make some of that safe:
979 /// assert_eq!(
980 /// to_html_with_options(
981 /// "<iframe>",
982 /// &Options {
983 /// parse: ParseOptions::gfm(),
984 /// compile: CompileOptions {
985 /// allow_dangerous_html: true,
986 /// gfm_tagfilter: true,
987 /// ..CompileOptions::default()
988 /// }
989 /// }
990 /// )?,
991 /// "<iframe>"
992 /// );
993 /// # Ok(())
994 /// # }
995 /// ```
996 ///
997 /// ## References
998 ///
999 /// * [*§ 6.1 Disallowed Raw HTML (extension)* in GFM](https://github.github.com/gfm/#disallowed-raw-html-extension-)
1000 /// * [`cmark-gfm#extensions/tagfilter.c`](https://github.com/github/cmark-gfm/blob/master/extensions/tagfilter.c)
1001 pub gfm_tagfilter: bool,
1002
1003 /// Whether to add unique `id` attributes to headings for in-page links
1004 /// and tables of contents.
1005 ///
1006 /// IDs use the same text normalization and duplicate suffixes as
1007 /// [`mdast::Headings::extract`][crate::mdast::Headings::extract].
1008 ///
1009 /// The default is `false`.
1010 pub heading_ids: bool,
1011}
1012
1013impl CompileOptions {
1014 /// GFM.
1015 ///
1016 /// GFM stands for **GitHub flavored markdown**.
1017 /// On the compilation side, GFM turns on the GFM tag filter.
1018 /// The tagfilter is useless, but it’s included here for consistency, and
1019 /// this method exists for parity to parse options.
1020 ///
1021 /// For more information, see the GFM specification:
1022 /// <https://github.github.com/gfm/>.
1023 pub fn gfm() -> Self {
1024 Self {
1025 gfm_tagfilter: true,
1026 ..Self::default()
1027 }
1028 }
1029}
1030
1031/// Configuration that describes how to parse from markdown.
1032///
1033/// You can use this:
1034///
1035/// * To control what markdown constructs are turned on and off
1036/// * To control some of those constructs
1037/// * To add support for certain programming languages when parsing MDX
1038///
1039/// In most cases, you will want to use the default trait or `gfm` method.
1040///
1041/// ## Examples
1042///
1043/// ```
1044/// use socketry_markdown::ParseOptions;
1045/// # fn main() {
1046///
1047/// // Use the default trait to parse markdown according to `CommonMark`:
1048/// let commonmark = ParseOptions::default();
1049///
1050/// // Use the `gfm` method to parse markdown according to GFM:
1051/// let gfm = ParseOptions::gfm();
1052/// # }
1053/// ```
1054#[allow(clippy::struct_excessive_bools)]
1055#[cfg_attr(
1056 feature = "serde",
1057 derive(serde::Serialize, serde::Deserialize),
1058 serde(default, rename_all = "camelCase")
1059)]
1060pub struct ParseOptions {
1061 // Note: when adding fields, don’t forget to add them to `fmt::Debug` below.
1062 /// Which constructs to enable and disable.
1063 ///
1064 /// The default is to follow `CommonMark`.
1065 ///
1066 /// ## Examples
1067 ///
1068 /// ```
1069 /// use socketry_markdown::{to_html, to_html_with_options, Constructs, Options, ParseOptions};
1070 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
1071 ///
1072 /// // `markdown-rs` follows CommonMark by default:
1073 /// assert_eq!(
1074 /// to_html(" indented code?"),
1075 /// "<pre><code>indented code?\n</code></pre>"
1076 /// );
1077 ///
1078 /// // Pass `constructs` to choose what to enable and disable:
1079 /// assert_eq!(
1080 /// to_html_with_options(
1081 /// " indented code?",
1082 /// &Options {
1083 /// parse: ParseOptions {
1084 /// constructs: Constructs {
1085 /// code_indented: false,
1086 /// ..Constructs::default()
1087 /// },
1088 /// ..ParseOptions::default()
1089 /// },
1090 /// ..Options::default()
1091 /// }
1092 /// )?,
1093 /// "<p>indented code?</p>"
1094 /// );
1095 /// # Ok(())
1096 /// # }
1097 /// ```
1098 #[cfg_attr(feature = "serde", serde(default))]
1099 pub constructs: Constructs,
1100
1101 /// Whether to parse a language prefix, such as `ruby:`, before inline code.
1102 ///
1103 /// When enabled, the prefix is removed from the surrounding text and
1104 /// stored as the inline code node's `lang` value. The default is `false`.
1105 #[cfg_attr(feature = "serde", serde(default))]
1106 pub inline_code_info: bool,
1107
1108 /// Whether indented type 6 and type 7 HTML blocks continue across blank
1109 /// lines when later content keeps a consistent indentation.
1110 ///
1111 /// The default is `false`, which follows `CommonMark`'s blank-line rule.
1112 #[cfg_attr(feature = "serde", serde(default))]
1113 pub html_block_blank_lines: bool,
1114
1115 /// Whether `:` is allowed in HTML tag names for namespace prefixes such
1116 /// as `svg:circle`.
1117 ///
1118 /// The default is `false` to preserve `CommonMark` parsing of text such as
1119 /// `<m:abc>`.
1120 #[cfg_attr(feature = "serde", serde(default))]
1121 pub html_tag_namespaces: bool,
1122
1123 /// Whether to support GFM strikethrough with a single tilde
1124 ///
1125 /// This option does nothing if `gfm_strikethrough` is not turned on in
1126 /// `constructs`.
1127 /// This option does not affect strikethrough with double tildes.
1128 ///
1129 /// The default is `true`, which follows how markdown on `github.com`
1130 /// works, as strikethrough with single tildes is supported.
1131 /// Pass `false`, to follow the GFM spec more strictly, by not allowing
1132 /// strikethrough with single tildes.
1133 ///
1134 /// ## Examples
1135 ///
1136 /// ```
1137 /// use socketry_markdown::{to_html_with_options, Constructs, Options, ParseOptions};
1138 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
1139 ///
1140 /// // `markdown-rs` supports single tildes by default:
1141 /// assert_eq!(
1142 /// to_html_with_options(
1143 /// "~a~",
1144 /// &Options {
1145 /// parse: ParseOptions {
1146 /// constructs: Constructs::gfm(),
1147 /// ..ParseOptions::default()
1148 /// },
1149 /// ..Options::default()
1150 /// }
1151 /// )?,
1152 /// "<p><del>a</del></p>"
1153 /// );
1154 ///
1155 /// // Pass `gfm_strikethrough_single_tilde: false` to turn that off:
1156 /// assert_eq!(
1157 /// to_html_with_options(
1158 /// "~a~",
1159 /// &Options {
1160 /// parse: ParseOptions {
1161 /// constructs: Constructs::gfm(),
1162 /// gfm_strikethrough_single_tilde: false,
1163 /// ..ParseOptions::default()
1164 /// },
1165 /// ..Options::default()
1166 /// }
1167 /// )?,
1168 /// "<p>~a~</p>"
1169 /// );
1170 /// # Ok(())
1171 /// # }
1172 /// ```
1173 #[cfg_attr(feature = "serde", serde(default))]
1174 pub gfm_strikethrough_single_tilde: bool,
1175
1176 /// Whether to support math (text) with a single dollar
1177 ///
1178 /// This option does nothing if `math_text` is not turned on in
1179 /// `constructs`.
1180 /// This option does not affect math (text) with two or more dollars.
1181 ///
1182 /// The default is `true`, which is more close to how code (text) and
1183 /// Pandoc work, as it allows math with a single dollar to form.
1184 /// However, single dollars can interfere with “normal” dollars in text.
1185 /// Pass `false`, to only allow math (text) to form when two or more
1186 /// dollars are used.
1187 /// If you pass `false`, you can still use two or more dollars for text
1188 /// math.
1189 ///
1190 /// ## Examples
1191 ///
1192 /// ```
1193 /// use socketry_markdown::{to_html_with_options, Constructs, Options, ParseOptions};
1194 /// # fn main() -> Result<(), socketry_markdown::message::Message> {
1195 ///
1196 /// // `markdown-rs` supports single dollars by default:
1197 /// assert_eq!(
1198 /// to_html_with_options(
1199 /// "$a$",
1200 /// &Options {
1201 /// parse: ParseOptions {
1202 /// constructs: Constructs {
1203 /// math_text: true,
1204 /// ..Constructs::default()
1205 /// },
1206 /// ..ParseOptions::default()
1207 /// },
1208 /// ..Options::default()
1209 /// }
1210 /// )?,
1211 /// "<p><code class=\"language-math math-inline\">a</code></p>"
1212 /// );
1213 ///
1214 /// // Pass `math_text_single_dollar: false` to turn that off:
1215 /// assert_eq!(
1216 /// to_html_with_options(
1217 /// "$a$",
1218 /// &Options {
1219 /// parse: ParseOptions {
1220 /// constructs: Constructs {
1221 /// math_text: true,
1222 /// ..Constructs::default()
1223 /// },
1224 /// math_text_single_dollar: false,
1225 /// ..ParseOptions::default()
1226 /// },
1227 /// ..Options::default()
1228 /// }
1229 /// )?,
1230 /// "<p>$a$</p>"
1231 /// );
1232 /// # Ok(())
1233 /// # }
1234 /// ```
1235 #[cfg_attr(feature = "serde", serde(default))]
1236 pub math_text_single_dollar: bool,
1237
1238 /// Function to parse expressions with.
1239 ///
1240 /// This function can be used to add support for arbitrary programming
1241 /// languages within expressions.
1242 ///
1243 /// It only makes sense to pass this when compiling to a syntax tree
1244 /// with [`to_mdast()`][crate::to_mdast()].
1245 ///
1246 /// For an example that adds support for JavaScript with SWC, see
1247 /// `tests/test_utils/mod.rs`.
1248 #[cfg_attr(feature = "serde", serde(skip))]
1249 pub mdx_expression_parse: Option<Box<MdxExpressionParse>>,
1250
1251 /// Function to parse ESM with.
1252 ///
1253 /// This function can be used to add support for arbitrary programming
1254 /// languages within ESM blocks, however, the keywords (`export`,
1255 /// `import`) are currently hardcoded JavaScript-specific.
1256 ///
1257 /// > 👉 **Note**: please raise an issue if you’re interested in working on
1258 /// > MDX that is aware of, say, Rust, or other programming languages.
1259 ///
1260 /// It only makes sense to pass this when compiling to a syntax tree
1261 /// with [`to_mdast()`][crate::to_mdast()].
1262 ///
1263 /// For an example that adds support for JavaScript with SWC, see
1264 /// `tests/test_utils/mod.rs`.
1265 #[cfg_attr(feature = "serde", serde(skip))]
1266 pub mdx_esm_parse: Option<Box<MdxEsmParse>>,
1267 // Note: when adding fields, don’t forget to add them to `fmt::Debug` below.
1268}
1269
1270impl fmt::Debug for ParseOptions {
1271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1272 f.debug_struct("ParseOptions")
1273 .field("constructs", &self.constructs)
1274 .field("inline_code_info", &self.inline_code_info)
1275 .field("html_block_blank_lines", &self.html_block_blank_lines)
1276 .field("html_tag_namespaces", &self.html_tag_namespaces)
1277 .field(
1278 "gfm_strikethrough_single_tilde",
1279 &self.gfm_strikethrough_single_tilde,
1280 )
1281 .field("math_text_single_dollar", &self.math_text_single_dollar)
1282 .field(
1283 "mdx_expression_parse",
1284 &self.mdx_expression_parse.as_ref().map(|_d| "[Function]"),
1285 )
1286 .field(
1287 "mdx_esm_parse",
1288 &self.mdx_esm_parse.as_ref().map(|_d| "[Function]"),
1289 )
1290 .finish()
1291 }
1292}
1293
1294impl Default for ParseOptions {
1295 /// `CommonMark` defaults.
1296 fn default() -> Self {
1297 Self {
1298 constructs: Constructs::default(),
1299 inline_code_info: false,
1300 html_block_blank_lines: false,
1301 html_tag_namespaces: false,
1302 gfm_strikethrough_single_tilde: true,
1303 math_text_single_dollar: true,
1304 mdx_expression_parse: None,
1305 mdx_esm_parse: None,
1306 }
1307 }
1308}
1309
1310impl ParseOptions {
1311 /// GFM.
1312 ///
1313 /// GFM stands for GitHub flavored markdown.
1314 /// GFM extends `CommonMark` and adds support for autolink literals,
1315 /// footnotes, strikethrough, tables, and tasklists.
1316 ///
1317 /// For more information, see the GFM specification:
1318 /// <https://github.github.com/gfm/>
1319 pub fn gfm() -> Self {
1320 Self {
1321 constructs: Constructs::gfm(),
1322 ..Self::default()
1323 }
1324 }
1325
1326 /// MDX.
1327 ///
1328 /// This turns on `CommonMark`, turns off some conflicting constructs
1329 /// (autolinks, code (indented), and HTML), and turns on MDX (ESM,
1330 /// expressions, and JSX).
1331 ///
1332 /// For more information, see the MDX website:
1333 /// <https://mdxjs.com>.
1334 ///
1335 /// > 👉 **Note**: to support ESM, you *must* pass
1336 /// > [`mdx_esm_parse`][MdxEsmParse] in [`ParseOptions`][] too.
1337 /// > Otherwise, ESM is treated as normal markdown.
1338 /// >
1339 /// > You *can* pass
1340 /// > [`mdx_expression_parse`][MdxExpressionParse]
1341 /// > to parse expressions according to a certain grammar (typically, a
1342 /// > programming language).
1343 /// > Otherwise, expressions are parsed with a basic algorithm that only
1344 /// > cares about braces.
1345 pub fn mdx() -> Self {
1346 Self {
1347 constructs: Constructs::mdx(),
1348 ..Self::default()
1349 }
1350 }
1351}
1352
1353/// Configuration that describes how to parse from markdown and compile to
1354/// HTML.
1355///
1356/// In most cases, you will want to use the default trait or `gfm` method.
1357///
1358/// ## Examples
1359///
1360/// ```
1361/// use socketry_markdown::Options;
1362/// # fn main() {
1363///
1364/// // Use the default trait to compile markdown to HTML according to `CommonMark`:
1365/// let commonmark = Options::default();
1366///
1367/// // Use the `gfm` method to compile markdown to HTML according to GFM:
1368/// let gfm = Options::gfm();
1369/// # }
1370/// ```
1371#[allow(clippy::struct_excessive_bools)]
1372#[derive(Debug, Default)]
1373#[cfg_attr(
1374 feature = "serde",
1375 derive(serde::Serialize, serde::Deserialize),
1376 serde(default)
1377)]
1378pub struct Options {
1379 /// Configuration that describes how to parse from markdown.
1380 pub parse: ParseOptions,
1381 /// Configuration that describes how to compile to HTML.
1382 pub compile: CompileOptions,
1383}
1384
1385impl Options {
1386 /// GFM.
1387 ///
1388 /// GFM stands for GitHub flavored markdown.
1389 /// GFM extends `CommonMark` and adds support for autolink literals,
1390 /// footnotes, strikethrough, tables, and tasklists.
1391 /// On the compilation side, GFM turns on the GFM tag filter.
1392 /// The tagfilter is useless, but it’s included here for consistency.
1393 ///
1394 /// For more information, see the GFM specification:
1395 /// <https://github.github.com/gfm/>
1396 pub fn gfm() -> Self {
1397 Self {
1398 parse: ParseOptions::gfm(),
1399 compile: CompileOptions::gfm(),
1400 }
1401 }
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use super::*;
1407 use crate::util::mdx::Signal;
1408 use alloc::format;
1409
1410 #[test]
1411 fn test_constructs() {
1412 Constructs::default();
1413 Constructs::gfm();
1414 Constructs::mdx();
1415
1416 let constructs = Constructs::default();
1417 assert!(constructs.attention, "should default to `CommonMark` (1)");
1418 assert!(
1419 !constructs.gfm_autolink_literal,
1420 "should default to `CommonMark` (2)"
1421 );
1422 assert!(
1423 !constructs.mdx_jsx_flow,
1424 "should default to `CommonMark` (3)"
1425 );
1426 assert!(
1427 !constructs.frontmatter,
1428 "should default to `CommonMark` (4)"
1429 );
1430
1431 let constructs = Constructs::gfm();
1432 assert!(constructs.attention, "should support `gfm` shortcut (1)");
1433 assert!(
1434 constructs.gfm_autolink_literal,
1435 "should support `gfm` shortcut (2)"
1436 );
1437 assert!(
1438 !constructs.mdx_jsx_flow,
1439 "should support `gfm` shortcut (3)"
1440 );
1441 assert!(!constructs.frontmatter, "should support `gfm` shortcut (4)");
1442
1443 let constructs = Constructs::mdx();
1444 assert!(constructs.attention, "should support `gfm` shortcut (1)");
1445 assert!(
1446 !constructs.gfm_autolink_literal,
1447 "should support `mdx` shortcut (2)"
1448 );
1449 assert!(constructs.mdx_jsx_flow, "should support `mdx` shortcut (3)");
1450 assert!(!constructs.frontmatter, "should support `mdx` shortcut (4)");
1451 }
1452
1453 #[test]
1454 fn test_parse_options() {
1455 ParseOptions::default();
1456 ParseOptions::gfm();
1457 ParseOptions::mdx();
1458
1459 let options = ParseOptions::default();
1460 assert!(
1461 options.constructs.attention,
1462 "should default to `CommonMark` (1)"
1463 );
1464 assert!(
1465 !options.constructs.gfm_autolink_literal,
1466 "should default to `CommonMark` (2)"
1467 );
1468 assert!(
1469 !options.constructs.mdx_jsx_flow,
1470 "should default to `CommonMark` (3)"
1471 );
1472
1473 let options = ParseOptions::gfm();
1474 assert!(
1475 options.constructs.attention,
1476 "should support `gfm` shortcut (1)"
1477 );
1478 assert!(
1479 options.constructs.gfm_autolink_literal,
1480 "should support `gfm` shortcut (2)"
1481 );
1482 assert!(
1483 !options.constructs.mdx_jsx_flow,
1484 "should support `gfm` shortcut (3)"
1485 );
1486
1487 let options = ParseOptions::mdx();
1488 assert!(
1489 options.constructs.attention,
1490 "should support `mdx` shortcut (1)"
1491 );
1492 assert!(
1493 !options.constructs.gfm_autolink_literal,
1494 "should support `mdx` shortcut (2)"
1495 );
1496 assert!(
1497 options.constructs.mdx_jsx_flow,
1498 "should support `mdx` shortcut (3)"
1499 );
1500
1501 assert_eq!(
1502 format!("{:?}", ParseOptions::default()),
1503 "ParseOptions { constructs: Constructs { attention: true, autolink: true, block_quote: true, character_escape: true, character_reference: true, code_indented: true, code_fenced: true, code_text: true, definition: true, frontmatter: false, gfm_autolink_literal: false, gfm_footnote_definition: false, gfm_label_start_footnote: false, gfm_strikethrough: false, gfm_table: false, gfm_task_list_item: false, hard_break_escape: true, hard_break_trailing: true, heading_atx: true, heading_setext: true, html_flow: true, html_text: true, label_start_image: true, label_start_link: true, label_end: true, list_item: true, math_flow: false, math_text: false, mdx_esm: false, mdx_expression_flow: false, mdx_expression_text: false, mdx_jsx_flow: false, mdx_jsx_text: false, thematic_break: true }, inline_code_info: false, html_block_blank_lines: false, html_tag_namespaces: false, gfm_strikethrough_single_tilde: true, math_text_single_dollar: true, mdx_expression_parse: None, mdx_esm_parse: None }",
1504 "should support `Debug` trait"
1505 );
1506 assert_eq!(
1507 format!("{:?}", ParseOptions {
1508 mdx_esm_parse: Some(Box::new(|_value| {
1509 Signal::Ok
1510 })),
1511 mdx_expression_parse: Some(Box::new(|_value, _kind| {
1512 Signal::Ok
1513 })),
1514 ..Default::default()
1515 }),
1516 "ParseOptions { constructs: Constructs { attention: true, autolink: true, block_quote: true, character_escape: true, character_reference: true, code_indented: true, code_fenced: true, code_text: true, definition: true, frontmatter: false, gfm_autolink_literal: false, gfm_footnote_definition: false, gfm_label_start_footnote: false, gfm_strikethrough: false, gfm_table: false, gfm_task_list_item: false, hard_break_escape: true, hard_break_trailing: true, heading_atx: true, heading_setext: true, html_flow: true, html_text: true, label_start_image: true, label_start_link: true, label_end: true, list_item: true, math_flow: false, math_text: false, mdx_esm: false, mdx_expression_flow: false, mdx_expression_text: false, mdx_jsx_flow: false, mdx_jsx_text: false, thematic_break: true }, inline_code_info: false, html_block_blank_lines: false, html_tag_namespaces: false, gfm_strikethrough_single_tilde: true, math_text_single_dollar: true, mdx_expression_parse: Some(\"[Function]\"), mdx_esm_parse: Some(\"[Function]\") }",
1517 "should support `Debug` trait on mdx functions"
1518 );
1519 }
1520
1521 #[test]
1522 fn test_compile_options() {
1523 CompileOptions::default();
1524 CompileOptions::gfm();
1525
1526 let options = CompileOptions::default();
1527 assert!(
1528 !options.allow_dangerous_html,
1529 "should default to safe `CommonMark` (1)"
1530 );
1531 assert!(
1532 !options.gfm_tagfilter,
1533 "should default to safe `CommonMark` (2)"
1534 );
1535
1536 let options = CompileOptions::gfm();
1537 assert!(
1538 !options.allow_dangerous_html,
1539 "should support safe `gfm` shortcut (1)"
1540 );
1541 assert!(
1542 options.gfm_tagfilter,
1543 "should support safe `gfm` shortcut (1)"
1544 );
1545 }
1546
1547 #[test]
1548 fn test_options() {
1549 Options::default();
1550
1551 let options = Options::default();
1552 assert!(
1553 options.parse.constructs.attention,
1554 "should default to safe `CommonMark` (1)"
1555 );
1556 assert!(
1557 !options.parse.constructs.gfm_autolink_literal,
1558 "should default to safe `CommonMark` (2)"
1559 );
1560 assert!(
1561 !options.parse.constructs.mdx_jsx_flow,
1562 "should default to safe `CommonMark` (3)"
1563 );
1564 assert!(
1565 !options.compile.allow_dangerous_html,
1566 "should default to safe `CommonMark` (4)"
1567 );
1568
1569 let options = Options::gfm();
1570 assert!(
1571 options.parse.constructs.attention,
1572 "should support safe `gfm` shortcut (1)"
1573 );
1574 assert!(
1575 options.parse.constructs.gfm_autolink_literal,
1576 "should support safe `gfm` shortcut (2)"
1577 );
1578 assert!(
1579 !options.parse.constructs.mdx_jsx_flow,
1580 "should support safe `gfm` shortcut (3)"
1581 );
1582 assert!(
1583 !options.compile.allow_dangerous_html,
1584 "should support safe `gfm` shortcut (4)"
1585 );
1586 }
1587}