Skip to main content

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 &amp; 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 ![b](c) 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    ///         "![](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)",
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    ///         "![](javascript:alert(1))",
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, &lt;i&gt;venus&lt;/i&gt;!</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    ///     "&lt;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}