diff --git a/Cargo.toml b/Cargo.toml index 38bb9e04..55466560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,3 +78,7 @@ clap = { version = "4.0", optional = true, features = [ "string", "wrap_help", ] } + +[[example]] +name = "syntect" +required-features = [ "syntect" ] diff --git a/examples/s-expr.rs b/examples/s-expr.rs index db0a3552..1d01890c 100644 --- a/examples/s-expr.rs +++ b/examples/s-expr.rs @@ -14,7 +14,7 @@ const INDENT: usize = 4; const CLOSE_NEWLINE: bool = false; use comrak::nodes::{AstNode, NodeValue}; -use comrak::{parse_document, Arena, ExtensionOptions, Options}; +use comrak::{parse_document, Arena, ExtensionOptions, Options, WikiLinksMode}; use std::env; use std::error::Error; use std::fs::File; @@ -86,8 +86,7 @@ fn dump(source: &str) -> io::Result<()> { .multiline_block_quotes(true) .math_dollars(true) .math_code(true) - .wikilinks_title_after_pipe(true) - .wikilinks_title_before_pipe(true) + .wikilinks(WikiLinksMode::TitleFirst) .build(); let opts = Options { diff --git a/src/cm.rs b/src/cm.rs index aa2989e1..bc6b0e0f 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -6,7 +6,7 @@ use crate::nodes::{ use crate::nodes::{NodeList, TableAlignment}; #[cfg(feature = "shortcodes")] use crate::parser::shortcodes::NodeShortCode; -use crate::parser::Options; +use crate::parser::{Options, WikiLinksMode}; use crate::scanners; use crate::strings::trim_start_match; use crate::{nodes, Plugins}; @@ -761,12 +761,12 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> { fn format_wikilink(&mut self, nl: &NodeWikiLink, entering: bool) -> bool { if entering { write!(self, "[[").unwrap(); - if self.options.extension.wikilinks_title_after_pipe { + if self.options.extension.wikilinks == Some(WikiLinksMode::UrlFirst) { self.output(nl.url.as_bytes(), false, Escaping::Url); write!(self, "|").unwrap(); } } else { - if self.options.extension.wikilinks_title_before_pipe { + if self.options.extension.wikilinks == Some(WikiLinksMode::TitleFirst) { write!(self, "|").unwrap(); self.output(nl.url.as_bytes(), false, Escaping::Url); } diff --git a/src/lib.rs b/src/lib.rs index 3a2ba0ef..02692b72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,7 @@ pub use parser::{ parse_document, BrokenLinkCallback, BrokenLinkReference, ExtensionOptions, ExtensionOptionsBuilder, ListStyleType, Options, ParseOptions, ParseOptionsBuilder, Plugins, PluginsBuilder, RenderOptions, RenderOptionsBuilder, RenderPlugins, RenderPluginsBuilder, - ResolvedReference, URLRewriter, + ResolvedReference, URLRewriter, WikiLinksMode, }; pub use typed_arena::Arena; pub use xml::format_document as format_xml; diff --git a/src/main.rs b/src/main.rs index f9bd64ad..a767ca48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use std::path::PathBuf; use std::process; use clap::{Parser, ValueEnum}; -use comrak::{ExtensionOptions, ParseOptions, RenderOptions}; +use comrak::{ExtensionOptions, ParseOptions, RenderOptions, WikiLinksMode}; const EXIT_SUCCESS: i32 = 0; const EXIT_PARSE_CONFIG: i32 = 2; @@ -252,6 +252,21 @@ fn main() -> Result<(), Box> { let exts = &cli.extensions; + let wikilinks_title_after_pipe = exts.contains(&Extension::WikilinksTitleAfterPipe); + let wikilinks_title_before_pipe = exts.contains(&Extension::WikilinksTitleBeforePipe); + let wikilinks_mode = match (wikilinks_title_after_pipe, wikilinks_title_before_pipe) { + (false, false) => None, + (true, false) => Some(WikiLinksMode::UrlFirst), + (false, true) => Some(WikiLinksMode::TitleFirst), + (true, true) => { + eprintln!(concat!( + "cannot enable both wikilinks-title-after-pipe ", + "and wikilinks-title-before-pipe at the same time" + )); + process::exit(EXIT_PARSE_CONFIG); + } + }; + let extension = ExtensionOptions::builder() .strikethrough(exts.contains(&Extension::Strikethrough) || cli.gfm) .tagfilter(exts.contains(&Extension::Tagfilter) || cli.gfm) @@ -265,8 +280,7 @@ fn main() -> Result<(), Box> { .multiline_block_quotes(exts.contains(&Extension::MultilineBlockQuotes)) .math_dollars(exts.contains(&Extension::MathDollars)) .math_code(exts.contains(&Extension::MathCode)) - .wikilinks_title_after_pipe(exts.contains(&Extension::WikilinksTitleAfterPipe)) - .wikilinks_title_before_pipe(exts.contains(&Extension::WikilinksTitleBeforePipe)) + .maybe_wikilinks(wikilinks_mode) .underline(exts.contains(&Extension::Underline)) .subscript(exts.contains(&Extension::Subscript)) .spoiler(exts.contains(&Extension::Spoiler)) diff --git a/src/nodes.rs b/src/nodes.rs index 85407a7a..cd749c1e 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -181,6 +181,8 @@ pub enum NodeValue { MultilineBlockQuote(NodeMultilineBlockQuote), /// **Inline**. A character that has been [escaped](https://github.github.com/gfm/#backslash-escapes) + /// + /// Enabled with [`escaped_char_spans`](crate::RenderOptionsBuilder::escaped_char_spans). Escaped, /// **Inline**. A wikilink to some URL. diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs index 34e97405..725acc4a 100644 --- a/src/parser/inlines.rs +++ b/src/parser/inlines.rs @@ -21,6 +21,8 @@ use std::str; use typed_arena::Arena; use unicode_categories::UnicodeCategories; +use super::WikiLinksMode; + const MAXBACKTICKS: usize = 80; const MAX_LINK_LABEL_LENGTH: usize = 1000; const MAX_MATH_DOLLARS: usize = 2; @@ -235,8 +237,7 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { let mut wikilink_inl = None; - if (self.options.extension.wikilinks_title_after_pipe - || self.options.extension.wikilinks_title_before_pipe) + if self.options.extension.wikilinks.is_some() && !self.within_brackets && self.peek_char() == Some(&(b'[')) { @@ -1804,16 +1805,16 @@ impl<'a, 'r, 'o, 'd, 'i> Subject<'a, 'r, 'o, 'd, 'i> { if self.peek_char() == Some(&(b']')) && self.peek_char_n(1) == Some(&(b']')) { self.pos += 2; - if self.options.extension.wikilinks_title_after_pipe { - Some(WikilinkComponents { + match self.options.extension.wikilinks { + Some(WikiLinksMode::UrlFirst) => Some(WikilinkComponents { url: left, link_label: Some((right, right_startpos + 1, self.pos - 3)), - }) - } else { - Some(WikilinkComponents { + }), + Some(WikiLinksMode::TitleFirst) => Some(WikilinkComponents { url: right, link_label: Some((left, left_startpos + 1, right_startpos - 1)), - }) + }), + None => unreachable!(), } } else { self.pos = left_startpos; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 56d597a4..e59c9ba2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -192,6 +192,21 @@ where } } +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +/// Selects between wikilinks with the title first or the URL first. +/// +/// See [`ExtensionOptions::wikilinks`]. +pub enum WikiLinksMode { + /// Indicates that the URL precedes the title. For example: `[[http://example.com|link + /// title]]`. + UrlFirst, + + /// Indicates that the title precedes the URL. For example: `[[link title|http://example.com]]`. + TitleFirst, +} + #[non_exhaustive] #[derive(Default, Debug, Clone, Builder)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -466,37 +481,28 @@ pub struct ExtensionOptions { #[builder(default)] pub shortcodes: bool, - /// Enables wikilinks using title after pipe syntax + /// Enables wikilinks + /// + /// With [`WikiLinksMode::TitleFirst`]: /// /// ```` md - /// [[url|link label]] + /// [[link label|url]] /// ```` /// - /// ``` - /// # use comrak::{markdown_to_html, Options}; - /// let mut options = Options::default(); - /// options.extension.wikilinks_title_after_pipe = true; - /// assert_eq!(markdown_to_html("[[url|link label]]", &options), - /// "

link label

\n"); - /// ``` - #[builder(default)] - pub wikilinks_title_after_pipe: bool, - - /// Enables wikilinks using title before pipe syntax + /// With [`WikiLinksMode::UrlFirst`]: /// /// ```` md - /// [[link label|url]] + /// [[url|link label]] /// ```` /// /// ``` - /// # use comrak::{markdown_to_html, Options}; + /// # use comrak::{markdown_to_html, Options, WikiLinksMode}; /// let mut options = Options::default(); - /// options.extension.wikilinks_title_before_pipe = true; + /// options.extension.wikilinks = Some(WikiLinksMode::TitleFirst); /// assert_eq!(markdown_to_html("[[link label|url]]", &options), /// "

link label

\n"); /// ``` - #[builder(default)] - pub wikilinks_title_before_pipe: bool, + pub wikilinks: Option, /// Enables underlines using double underscores /// @@ -865,6 +871,8 @@ pub struct RenderOptions { /// let xml = markdown_to_commonmark_xml(input, &options); /// assert!(xml.contains("")); /// ``` + /// + /// [`experimental_inline_sourcepos`]: crate::RenderOptionsBuilder::experimental_inline_sourcepos #[builder(default)] pub sourcepos: bool, diff --git a/src/tests.rs b/src/tests.rs index e143709f..280bfa52 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -127,20 +127,35 @@ macro_rules! html_opts { ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr) => { html_opts!([$($optclass.$optname),*], $lhs, $rhs,) }; + ([$($optclass:ident.$optname:ident = $val:expr),*], $lhs:expr, $rhs:expr) => { + html_opts!([$($optclass.$optname = $val),*], $lhs, $rhs,) + }; ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr,) => { html_opts!([$($optclass.$optname),*], $lhs, $rhs, roundtrip) }; + ([$($optclass:ident.$optname:ident = $val:expr),*], $lhs:expr, $rhs:expr,) => { + html_opts!([$($optclass.$optname = $val),*], $lhs, $rhs, roundtrip) + }; ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, $rt:ident) => { html_opts!([$($optclass.$optname),*], $lhs, $rhs, $rt,) }; + ([$($optclass:ident.$optname:ident = $val:expr),*], $lhs:expr, $rhs:expr, $rt:ident) => { + html_opts!([$($optclass.$optname = $val),*], $lhs, $rhs, $rt,) + }; ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, roundtrip,) => { + html_opts!([$($optclass.$optname = true),*], $lhs, $rhs, roundtrip,) + }; + ([$($optclass:ident.$optname:ident = $val:expr),*], $lhs:expr, $rhs:expr, roundtrip,) => { $crate::tests::html_opts_i($lhs, $rhs, true, |opts| { - $(opts.$optclass.$optname = true;)* + $(opts.$optclass.$optname = $val;)* }); }; ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, no_roundtrip,) => { + html_opts!([$($optclass.$optname = true),*], $lhs, $rhs, no_roundtrip,) + }; + ([$($optclass:ident.$optname:ident = $val:expr),*], $lhs:expr, $rhs:expr, no_roundtrip,) => { $crate::tests::html_opts_i($lhs, $rhs, false, |opts| { - $(opts.$optclass.$optname = true;)* + $(opts.$optclass.$optname = $val;)* }); }; } @@ -312,13 +327,20 @@ macro_rules! assert_ast_match { $amt ) }; - ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt) => { + ([ $( $optclass:ident.$optname:ident = $val:expr ),* ], $( $md:literal )+, $amt:tt) => { crate::tests::assert_ast_match_i( concat!( $( $md ),+ ), ast!($amt), - |#[allow(unused_variables)] opts| {$(opts.$optclass.$optname = true;)*}, + |#[allow(unused_variables)] opts| {$(opts.$optclass.$optname = $val;)*}, ); }; + ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt) => { + assert_ast_match!( + [ $( $optclass.$optname = true),* ], + $( $md )+, + $amt + ) + }; } pub(crate) use assert_ast_match; diff --git a/src/tests/api.rs b/src/tests/api.rs index cfd0c785..9ee4facf 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex}; -use parser::BrokenLinkReference; +use parser::{BrokenLinkReference, WikiLinksMode}; use crate::{ adapters::{HeadingAdapter, HeadingMeta, SyntaxHighlighterAdapter}, @@ -69,8 +69,7 @@ fn exercise_full_api() { let extension = extension.shortcodes(true); let _extension = extension - .wikilinks_title_after_pipe(true) - .wikilinks_title_before_pipe(true) + .wikilinks(WikiLinksMode::UrlFirst) .underline(true) .subscript(true) .spoiler(true) diff --git a/src/tests/commonmark.rs b/src/tests/commonmark.rs index 5d6e2e6c..b1473f52 100644 --- a/src/tests/commonmark.rs +++ b/src/tests/commonmark.rs @@ -4,6 +4,7 @@ use self::nodes::{Ast, LineColumn, ListType, NodeList}; use super::*; use ntest::test_case; +use parser::WikiLinksMode; #[test] fn commonmark_removes_redundant_strong() { @@ -83,7 +84,7 @@ fn math(markdown: &str, cm: &str) { #[test_case("This [[url|link label]] that", "This [[url|link%20label]] that\n")] fn wikilinks(markdown: &str, cm: &str) { let mut options = Options::default(); - options.extension.wikilinks_title_before_pipe = true; + options.extension.wikilinks = Some(WikiLinksMode::TitleFirst); commonmark(markdown, cm, Some(&options)); } diff --git a/src/tests/wikilinks.rs b/src/tests/wikilinks.rs index 039816e6..3f31f141 100644 --- a/src/tests/wikilinks.rs +++ b/src/tests/wikilinks.rs @@ -1,16 +1,17 @@ use super::*; +use crate::WikiLinksMode; #[test] fn wikilinks_does_not_unescape_html_entities_in_link_label() { html_opts!( - [extension.wikilinks_title_after_pipe], + [extension.wikilinks = Some(WikiLinksMode::UrlFirst)], concat!("This is [[<script>alert(0)</script>|a <link]]",), concat!("

This is a <link

\n"), no_roundtrip, ); html_opts!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], concat!("This is [[a <link|<script>alert(0)</script>]]",), concat!("

This is a <link

\n"), no_roundtrip, @@ -20,13 +21,13 @@ fn wikilinks_does_not_unescape_html_entities_in_link_label() { #[test] fn wikilinks_sanitizes_the_href_attribute_case_1() { html_opts!( - [extension.wikilinks_title_after_pipe], + [extension.wikilinks = Some(WikiLinksMode::UrlFirst)], concat!("[[http:\'\"injected=attribute><img/src=\"0\"onerror=\"alert(0)\">https://example.com|a]]",), concat!("

a

\n"), ); html_opts!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], concat!("[[a|http:\'\"injected=attribute><img/src=\"0\"onerror=\"alert(0)\">https://example.com]]",), concat!("

a

\n"), ); @@ -35,13 +36,13 @@ fn wikilinks_sanitizes_the_href_attribute_case_1() { #[test] fn wikilinks_sanitizes_the_href_attribute_case_2() { html_opts!( - [extension.wikilinks_title_after_pipe], + [extension.wikilinks = Some(WikiLinksMode::UrlFirst)], concat!("[[\'\"><svg><i/class=gl-show-field-errors><input/title=\"<script>alert(0)</script>\"/></svg>https://example.com|a]]",), concat!("

a

\n"), ); html_opts!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], concat!("[[a|\'\"><svg><i/class=gl-show-field-errors><input/title=\"<script>alert(0)</script>\"/></svg>https://example.com]]",), concat!("

a

\n"), ); @@ -50,7 +51,7 @@ fn wikilinks_sanitizes_the_href_attribute_case_2() { #[test] fn wikilinks_title_escape_chars() { html_opts!( - [extension.wikilinks_title_before_pipe, render.escaped_char_spans], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst), render.escaped_char_spans = true], concat!("[[Name \\[of\\] page|http://example.com]]",), concat!("

Name [of] page

\n"), no_roundtrip, @@ -61,8 +62,8 @@ fn wikilinks_title_escape_chars() { fn wikilinks_supercedes_relaxed_autolinks() { html_opts!( [ - extension.wikilinks_title_after_pipe, - parse.relaxed_autolinks + extension.wikilinks = Some(WikiLinksMode::UrlFirst), + parse.relaxed_autolinks = true ], concat!("[[http://example.com]]",), concat!( @@ -72,8 +73,8 @@ fn wikilinks_supercedes_relaxed_autolinks() { html_opts!( [ - extension.wikilinks_title_before_pipe, - parse.relaxed_autolinks + extension.wikilinks = Some(WikiLinksMode::TitleFirst), + parse.relaxed_autolinks = true ], concat!("[[http://example.com]]",), concat!( @@ -85,7 +86,10 @@ fn wikilinks_supercedes_relaxed_autolinks() { #[test] fn wikilinks_only_url_in_tables() { html_opts!( - [extension.wikilinks_title_after_pipe, extension.table], + [ + extension.wikilinks = Some(WikiLinksMode::UrlFirst), + extension.table = true + ], concat!("| header |\n", "| ------- |\n", "| [[url]] |\n",), concat!( "\n", @@ -104,7 +108,10 @@ fn wikilinks_only_url_in_tables() { ); html_opts!( - [extension.wikilinks_title_before_pipe, extension.table], + [ + extension.wikilinks = Some(WikiLinksMode::TitleFirst), + extension.table = true + ], concat!("| header |\n", "| ------- |\n", "| [[url]] |\n",), concat!( "
\n", @@ -126,7 +133,10 @@ fn wikilinks_only_url_in_tables() { #[test] fn wikilinks_full_in_tables_not_supported() { html_opts!( - [extension.wikilinks_title_after_pipe, extension.table], + [ + extension.wikilinks = Some(WikiLinksMode::UrlFirst), + extension.table = true + ], concat!("| header |\n", "| ------- |\n", "| [[url|link label]] |\n",), concat!( "
\n", @@ -145,7 +155,10 @@ fn wikilinks_full_in_tables_not_supported() { ); html_opts!( - [extension.wikilinks_title_before_pipe, extension.table], + [ + extension.wikilinks = Some(WikiLinksMode::TitleFirst), + extension.table = true + ], concat!("| header |\n", "| ------- |\n", "| [[link label|url]] |\n",), concat!( "
\n", @@ -170,7 +183,7 @@ fn wikilinks_exceeds_label_limit() { let expected = format!("

{}

\n", long_label); html_opts!( - [extension.wikilinks_title_after_pipe], + [extension.wikilinks = Some(WikiLinksMode::UrlFirst)], &long_label, &expected, ); @@ -179,7 +192,10 @@ fn wikilinks_exceeds_label_limit() { #[test] fn wikilinks_autolinker_ignored() { html_opts!( - [extension.wikilinks_title_after_pipe, extension.autolink], + [ + extension.wikilinks = Some(WikiLinksMode::UrlFirst), + extension.autolink = true + ], concat!("[[http://example.com]]",), concat!( "

http://example.com

\n" @@ -187,7 +203,10 @@ fn wikilinks_autolinker_ignored() { ); html_opts!( - [extension.wikilinks_title_before_pipe, extension.autolink], + [ + extension.wikilinks = Some(WikiLinksMode::TitleFirst), + extension.autolink = true + ], concat!("[[http://example.com]]",), concat!( "

http://example.com

\n" @@ -198,7 +217,7 @@ fn wikilinks_autolinker_ignored() { #[test] fn sourcepos() { assert_ast_match!( - [extension.wikilinks_title_after_pipe], + [extension.wikilinks = Some(WikiLinksMode::UrlFirst)], "This [[http://example.com|link label]] that\n", (document (1:1-1:43) [ (paragraph (1:1-1:43) [ @@ -212,7 +231,7 @@ fn sourcepos() { ); assert_ast_match!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], "This [[link label|http://example.com]] that\n", (document (1:1-1:43) [ (paragraph (1:1-1:43) [ @@ -226,7 +245,7 @@ fn sourcepos() { ); assert_ast_match!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], "This [[http://example.com]] that\n", (document (1:1-1:32) [ (paragraph (1:1-1:32) [ @@ -240,7 +259,7 @@ fn sourcepos() { ); assert_ast_match!( - [extension.wikilinks_title_before_pipe], + [extension.wikilinks = Some(WikiLinksMode::TitleFirst)], "This [[link\\[label|http://example.com]] that\n", (document (1:1-1:44) [ (paragraph (1:1-1:44) [