diff --git a/src/configuration.rs b/src/configuration.rs index 38af0a5..4f0281f 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -31,6 +31,11 @@ use alloc::{boxed::Box, fmt, string::String}; /// ``` #[allow(clippy::struct_excessive_bools)] #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] pub struct Constructs { /// Attention. /// @@ -459,15 +464,69 @@ impl Constructs { /// /// // In French: /// let enFrançais = CompileOptions { -/// gfm_footnote_label: Some("Notes de bas de page".into()), /// gfm_footnote_back_label: Some("Arrière".into()), +/// gfm_footnote_label: Some("Notes de bas de page".into()), /// ..CompileOptions::default() /// }; /// # } /// ``` #[allow(clippy::struct_excessive_bools)] #[derive(Clone, Debug, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(default, rename_all = "camelCase") +)] pub struct CompileOptions { + /// Whether to allow all values in images. + /// + /// The default is `false`, + /// which lets `allow_dangerous_protocol` control protocol safety for + /// both links and images. + /// + /// Pass `true` to allow all values as `src` on images, + /// regardless of `allow_dangerous_protocol`. + /// This is safe because the + /// [HTML specification][whatwg-html-image-processing] + /// does not allow executable code in images. + /// + /// [whatwg-html-image-processing]: https://html.spec.whatwg.org/multipage/images.html#images-processing-model + /// + /// ## Examples + /// + /// ``` + /// use markdown::{to_html_with_options, CompileOptions, Options}; + /// # fn main() -> Result<(), markdown::message::Message> { + /// + /// // By default, some protocols in image sources are dropped: + /// assert_eq!( + /// to_html_with_options( + /// "![](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)", + /// &Options::default() + /// )?, + /// "

\"\"

" + /// ); + /// + /// // Turn `allow_any_img_src` on to allow all values as `src` on images. + /// // This is safe because browsers do not execute code in images. + /// assert_eq!( + /// to_html_with_options( + /// "![](javascript:alert(1))", + /// &Options { + /// compile: CompileOptions { + /// allow_any_img_src: true, + /// ..CompileOptions::default() + /// }, + /// ..Options::default() + /// } + /// )?, + /// "

\"\"

" + /// ); + /// # Ok(()) + /// # } + /// ``` + pub allow_any_img_src: bool, + /// Whether to allow (dangerous) HTML. /// /// The default is `false`, which still parses the HTML according to @@ -563,55 +622,6 @@ pub struct CompileOptions { /// ``` pub allow_dangerous_protocol: bool, - /// Whether to allow all values in images. - /// - /// The default is `false`, - /// which lets `allow_dangerous_protocol` control protocol safety for - /// both links and images. - /// - /// Pass `true` to allow all values as `src` on images, - /// regardless of `allow_dangerous_protocol`. - /// This is safe because the - /// [HTML specification][whatwg-html-image-processing] - /// does not allow executable code in images. - /// - /// [whatwg-html-image-processing]: https://html.spec.whatwg.org/multipage/images.html#images-processing-model - /// - /// ## Examples - /// - /// ``` - /// use markdown::{to_html_with_options, CompileOptions, Options}; - /// # fn main() -> Result<(), markdown::message::Message> { - /// - /// // By default, some protocols in image sources are dropped: - /// assert_eq!( - /// to_html_with_options( - /// "![](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)", - /// &Options::default() - /// )?, - /// "

\"\"

" - /// ); - /// - /// // Turn `allow_any_img_src` on to allow all values as `src` on images. - /// // This is safe because browsers do not execute code in images. - /// assert_eq!( - /// to_html_with_options( - /// "![](javascript:alert(1))", - /// &Options { - /// compile: CompileOptions { - /// allow_any_img_src: true, - /// ..CompileOptions::default() - /// }, - /// ..Options::default() - /// } - /// )?, - /// "

\"\"

" - /// ); - /// # Ok(()) - /// # } - /// ``` - pub allow_any_img_src: bool, - // To do: `doc_markdown` is broken. #[allow(clippy::doc_markdown)] /// Default line ending to use when compiling to HTML, for line endings not @@ -658,16 +668,14 @@ pub struct CompileOptions { /// ``` pub default_line_ending: LineEnding, - /// Textual label to use for the footnotes section. + /// Textual label to describe the backreference back to footnote calls. /// - /// The default value is `"Footnotes"`. + /// The default value is `"Back to content"`. /// Change it when the markdown is not in English. /// - /// This label is typically hidden visually (assuming a `sr-only` CSS class - /// is defined that does that), and thus affects screen readers only. - /// If you do have such a class, but want to show this section to everyone, - /// pass different attributes with the `gfm_footnote_label_attributes` - /// option. + /// This label is used in the `aria-label` attribute on each backreference + /// (the `↩` links). + /// It affects users of assistive technology. /// /// ## Examples /// @@ -675,7 +683,7 @@ pub struct CompileOptions { /// use markdown::{to_html_with_options, CompileOptions, Options, ParseOptions}; /// # fn main() -> Result<(), markdown::message::Message> { /// - /// // `"Footnotes"` is used by default: + /// // `"Back to content"` is used by default: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", @@ -684,35 +692,46 @@ pub struct CompileOptions { /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// - /// // Pass `gfm_footnote_label` to use something else: + /// // Pass `gfm_footnote_back_label` to use something else: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", /// &Options { /// parse: ParseOptions::gfm(), /// compile: CompileOptions { - /// gfm_footnote_label: Some("Notes de bas de page".into()), + /// gfm_footnote_back_label: Some("Arrière".into()), /// ..CompileOptions::gfm() /// } /// } /// )?, - /// "

1

\n

Notes de bas de page

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" + /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// # Ok(()) /// # } /// ``` - pub gfm_footnote_label: Option, + pub gfm_footnote_back_label: Option, - /// HTML tag name to use for the footnote label element. + /// Prefix to use before the `id` attribute on footnotes to prevent them + /// from *clobbering*. /// - /// The default value is `"h2"`. - /// Change it to match your document structure. + /// The default is `"user-content-"`. + /// Pass `Some("".into())` for trusted markdown and when you are careful + /// with polyfilling. + /// You could pass a different prefix. /// - /// This label is typically hidden visually (assuming a `sr-only` CSS class - /// is defined that does that), and thus affects screen readers only. - /// If you do have such a class, but want to show this section to everyone, - /// pass different attributes with the `gfm_footnote_label_attributes` - /// option. + /// DOM clobbering is this: + /// + /// ```html + ///

+ /// + /// ``` + /// + /// The above example shows that elements are made available by browsers, + /// by their ID, on the `window` object. + /// This is a security risk because you might be expecting some other + /// variable at that place. + /// It can also break polyfills. + /// Using a prefix solves these problems. /// /// ## Examples /// @@ -720,7 +739,7 @@ pub struct CompileOptions { /// use markdown::{to_html_with_options, CompileOptions, Options, ParseOptions}; /// # fn main() -> Result<(), markdown::message::Message> { /// - /// // `"h2"` is used by default: + /// // `"user-content-"` is used by default: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", @@ -729,24 +748,24 @@ pub struct CompileOptions { /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// - /// // Pass `gfm_footnote_label_tag_name` to use something else: + /// // Pass `gfm_footnote_clobber_prefix` to use something else: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", /// &Options { /// parse: ParseOptions::gfm(), /// compile: CompileOptions { - /// gfm_footnote_label_tag_name: Some("h1".into()), + /// gfm_footnote_clobber_prefix: Some("".into()), /// ..CompileOptions::gfm() /// } /// } /// )?, - /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" + /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// # Ok(()) /// # } /// ``` - pub gfm_footnote_label_tag_name: Option, + pub gfm_footnote_clobber_prefix: Option, /// Attributes to use on the footnote label. /// @@ -796,14 +815,16 @@ pub struct CompileOptions { /// ``` pub gfm_footnote_label_attributes: Option, - /// Textual label to describe the backreference back to footnote calls. + /// HTML tag name to use for the footnote label element. /// - /// The default value is `"Back to content"`. - /// Change it when the markdown is not in English. + /// The default value is `"h2"`. + /// Change it to match your document structure. /// - /// This label is used in the `aria-label` attribute on each backreference - /// (the `↩` links). - /// It affects users of assistive technology. + /// This label is typically hidden visually (assuming a `sr-only` CSS class + /// is defined that does that), and thus affects screen readers only. + /// If you do have such a class, but want to show this section to everyone, + /// pass different attributes with the `gfm_footnote_label_attributes` + /// option. /// /// ## Examples /// @@ -811,7 +832,7 @@ pub struct CompileOptions { /// use markdown::{to_html_with_options, CompileOptions, Options, ParseOptions}; /// # fn main() -> Result<(), markdown::message::Message> { /// - /// // `"Back to content"` is used by default: + /// // `"h2"` is used by default: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", @@ -820,46 +841,35 @@ pub struct CompileOptions { /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// - /// // Pass `gfm_footnote_back_label` to use something else: + /// // Pass `gfm_footnote_label_tag_name` to use something else: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", /// &Options { /// parse: ParseOptions::gfm(), /// compile: CompileOptions { - /// gfm_footnote_back_label: Some("Arrière".into()), + /// gfm_footnote_label_tag_name: Some("h1".into()), /// ..CompileOptions::gfm() /// } /// } /// )?, - /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" + /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// # Ok(()) /// # } /// ``` - pub gfm_footnote_back_label: Option, + pub gfm_footnote_label_tag_name: Option, - /// Prefix to use before the `id` attribute on footnotes to prevent them - /// from *clobbering*. - /// - /// The default is `"user-content-"`. - /// Pass `Some("".into())` for trusted markdown and when you are careful - /// with polyfilling. - /// You could pass a different prefix. - /// - /// DOM clobbering is this: + /// Textual label to use for the footnotes section. /// - /// ```html - ///

- /// - /// ``` + /// The default value is `"Footnotes"`. + /// Change it when the markdown is not in English. /// - /// The above example shows that elements are made available by browsers, - /// by their ID, on the `window` object. - /// This is a security risk because you might be expecting some other - /// variable at that place. - /// It can also break polyfills. - /// Using a prefix solves these problems. + /// This label is typically hidden visually (assuming a `sr-only` CSS class + /// is defined that does that), and thus affects screen readers only. + /// If you do have such a class, but want to show this section to everyone, + /// pass different attributes with the `gfm_footnote_label_attributes` + /// option. /// /// ## Examples /// @@ -867,7 +877,7 @@ pub struct CompileOptions { /// use markdown::{to_html_with_options, CompileOptions, Options, ParseOptions}; /// # fn main() -> Result<(), markdown::message::Message> { /// - /// // `"user-content-"` is used by default: + /// // `"Footnotes"` is used by default: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", @@ -876,24 +886,24 @@ pub struct CompileOptions { /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// - /// // Pass `gfm_footnote_clobber_prefix` to use something else: + /// // Pass `gfm_footnote_label` to use something else: /// assert_eq!( /// to_html_with_options( /// "[^a]\n\n[^a]: b", /// &Options { /// parse: ParseOptions::gfm(), /// compile: CompileOptions { - /// gfm_footnote_clobber_prefix: Some("".into()), + /// gfm_footnote_label: Some("Notes de bas de page".into()), /// ..CompileOptions::gfm() /// } /// } /// )?, - /// "

1

\n

Footnotes

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" + /// "

1

\n

Notes de bas de page

\n
    \n
  1. \n

    b ↩

    \n
  2. \n
\n
\n" /// ); /// # Ok(()) /// # } /// ``` - pub gfm_footnote_clobber_prefix: Option, + pub gfm_footnote_label: Option, /// Whether or not GFM task list html `` items are enabled. /// @@ -1026,6 +1036,11 @@ impl CompileOptions { /// # } /// ``` #[allow(clippy::struct_excessive_bools)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(default, rename_all = "camelCase") +)] pub struct ParseOptions { // Note: when adding fields, don’t forget to add them to `fmt::Debug` below. /// Which constructs to enable and disable. @@ -1064,6 +1079,7 @@ pub struct ParseOptions { /// # Ok(()) /// # } /// ``` + #[cfg_attr(feature = "serde", serde(default))] pub constructs: Constructs, /// Whether to support GFM strikethrough with a single tilde @@ -1116,6 +1132,7 @@ pub struct ParseOptions { /// # Ok(()) /// # } /// ``` + #[cfg_attr(feature = "serde", serde(default))] pub gfm_strikethrough_single_tilde: bool, /// Whether to support math (text) with a single dollar @@ -1177,6 +1194,7 @@ pub struct ParseOptions { /// # Ok(()) /// # } /// ``` + #[cfg_attr(feature = "serde", serde(default))] pub math_text_single_dollar: bool, /// Function to parse expressions with. @@ -1189,6 +1207,7 @@ pub struct ParseOptions { /// /// For an example that adds support for JavaScript with SWC, see /// `tests/test_utils/mod.rs`. + #[cfg_attr(feature = "serde", serde(skip))] pub mdx_expression_parse: Option>, /// Function to parse ESM with. @@ -1205,6 +1224,7 @@ pub struct ParseOptions { /// /// For an example that adds support for JavaScript with SWC, see /// `tests/test_utils/mod.rs`. + #[cfg_attr(feature = "serde", serde(skip))] pub mdx_esm_parse: Option>, // Note: when adding fields, don’t forget to add them to `fmt::Debug` below. } @@ -1306,6 +1326,11 @@ impl ParseOptions { /// ``` #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Default)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(default) +)] pub struct Options { /// Configuration that describes how to parse from markdown. pub parse: ParseOptions, diff --git a/src/lib.rs b/src/lib.rs index 2032e90..81b7d07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! — enable logging (includes `dep:log`); //! you can show logs with `RUST_LOG=debug` //! * **`serde`** -//! — enable serde to serialize the AST (includes `dep:serde`) +//! — enable serde to serialize ASTs and configuration (includes `dep:serde`) #![no_std] #![deny(clippy::pedantic)] diff --git a/src/util/line_ending.rs b/src/util/line_ending.rs index be4d8a3..93068b3 100644 --- a/src/util/line_ending.rs +++ b/src/util/line_ending.rs @@ -16,6 +16,7 @@ use alloc::{str::FromStr, string::String}; /// # } /// ``` #[derive(Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LineEnding { /// Both a carriage return (`\r`) and a line feed (`\n`). /// @@ -25,6 +26,7 @@ pub enum LineEnding { /// a␍␊ /// b /// ``` + #[cfg_attr(feature = "serde", serde(rename = "\r\n"))] CarriageReturnLineFeed, /// Sole carriage return (`\r`). /// @@ -34,6 +36,7 @@ pub enum LineEnding { /// a␍ /// b /// ``` + #[cfg_attr(feature = "serde", serde(rename = "\r"))] CarriageReturn, /// Sole line feed (`\n`). /// @@ -44,6 +47,7 @@ pub enum LineEnding { /// b /// ``` #[default] + #[cfg_attr(feature = "serde", serde(rename = "\n"))] LineFeed, } diff --git a/tests/image.rs b/tests/image.rs index 2827daf..51c643d 100644 --- a/tests/image.rs +++ b/tests/image.rs @@ -240,8 +240,8 @@ fn image() -> Result<(), message::Message> { "![](javascript:alert(1))", &Options { compile: CompileOptions { - allow_dangerous_protocol: false, allow_any_img_src: true, + allow_dangerous_protocol: false, ..Default::default() }, ..Default::default() diff --git a/tests/serde.rs b/tests/serde.rs index 2edc9ac..b32da52 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -9,6 +9,45 @@ enum Error { Serde(serde_json::Error), } +#[test] +#[cfg(feature = "serde")] +fn serde_constructs() -> Result<(), Error> { + use pretty_assertions::assert_eq; + + assert_eq!( + serde_json::to_string(&Constructs::default()).unwrap(), + r#"{"attention":true,"autolink":true,"blockQuote":true,"characterEscape":true,"characterReference":true,"codeIndented":true,"codeFenced":true,"codeText":true,"definition":true,"frontmatter":false,"gfmAutolinkLiteral":false,"gfmFootnoteDefinition":false,"gfmLabelStartFootnote":false,"gfmStrikethrough":false,"gfmTable":false,"gfmTaskListItem":false,"hardBreakEscape":true,"hardBreakTrailing":true,"headingAtx":true,"headingSetext":true,"htmlFlow":true,"htmlText":true,"labelStartImage":true,"labelStartLink":true,"labelEnd":true,"listItem":true,"mathFlow":false,"mathText":false,"mdxEsm":false,"mdxExpressionFlow":false,"mdxExpressionText":false,"mdxJsxFlow":false,"mdxJsxText":false,"thematicBreak":true}"# + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "serde")] +fn serde_compile_options() -> Result<(), Error> { + use pretty_assertions::assert_eq; + + assert_eq!( + serde_json::to_string(&markdown::CompileOptions::gfm()).unwrap(), + r#"{"allowAnyImgSrc":false,"allowDangerousHtml":false,"allowDangerousProtocol":false,"defaultLineEnding":"\n","gfmFootnoteBackLabel":null,"gfmFootnoteClobberPrefix":null,"gfmFootnoteLabelAttributes":null,"gfmFootnoteLabelTagName":null,"gfmFootnoteLabel":null,"gfmTaskListItemCheckable":false,"gfmTagfilter":true}"# + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "serde")] +fn serde_parse_options() -> Result<(), Error> { + use pretty_assertions::assert_eq; + + assert_eq!( + serde_json::to_string(&ParseOptions::gfm()).unwrap(), + r#"{"constructs":{"attention":true,"autolink":true,"blockQuote":true,"characterEscape":true,"characterReference":true,"codeIndented":true,"codeFenced":true,"codeText":true,"definition":true,"frontmatter":false,"gfmAutolinkLiteral":true,"gfmFootnoteDefinition":true,"gfmLabelStartFootnote":true,"gfmStrikethrough":true,"gfmTable":true,"gfmTaskListItem":true,"hardBreakEscape":true,"hardBreakTrailing":true,"headingAtx":true,"headingSetext":true,"htmlFlow":true,"htmlText":true,"labelStartImage":true,"labelStartLink":true,"labelEnd":true,"listItem":true,"mathFlow":false,"mathText":false,"mdxEsm":false,"mdxExpressionFlow":false,"mdxExpressionText":false,"mdxJsxFlow":false,"mdxJsxText":false,"thematicBreak":true},"gfmStrikethroughSingleTilde":true,"mathTextSingleDollar":true}"# + ); + + Ok(()) +} + #[test] fn serde_blockquote() -> Result<(), Error> { assert_serde( @@ -680,9 +719,9 @@ fn serde_paragraph() -> Result<(), Error> { ) } -/// Assert serde of Mdast constructs. +/// Assert serde of mdast constructs. /// -/// Refer below links for the MDAST JSON construct types. +/// Refer below links for the mdast JSON construct types. /// * /// * /// * @@ -705,6 +744,7 @@ fn assert_serde(input: &str, expected: &str, options: ParseOptions) -> Result<() source, serde_json::from_value(actual_value).map_err(Error::Serde)? ); + Ok(()) } diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index 5a93046..a446d95 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -1,7 +1,5 @@ //! Bridge between `markdown-rs` and SWC. -extern crate markdown; - use crate::test_utils::swc_utils::{create_span, RewritePrefixContext}; use markdown::{MdxExpressionKind, MdxSignal}; use std::rc::Rc;