diff --git a/examples/s-expr.rs b/examples/s-expr.rs
index ecee3eb5..dc1d2e52 100644
--- a/examples/s-expr.rs
+++ b/examples/s-expr.rs
@@ -86,6 +86,8 @@ 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)
.build()
.unwrap();
diff --git a/fuzz/fuzz_targets/all_options.rs b/fuzz/fuzz_targets/all_options.rs
index 6b2b6df8..af9ed045 100644
--- a/fuzz/fuzz_targets/all_options.rs
+++ b/fuzz/fuzz_targets/all_options.rs
@@ -23,7 +23,9 @@ fuzz_target!(|s: &str| {
extension.math_code = true;
extension.front_matter_delimiter = Some("---".to_string());
extension.shortcodes = true;
-
+ extension.wikilinks_title_after_pipe = true;
+ extension.wikilinks_title_before_pipe = true;
+
let mut parse = ParseOptions::default();
parse.smart = true;
parse.default_info_string = Some("rust".to_string());
diff --git a/fuzz/fuzz_targets/quadratic.rs b/fuzz/fuzz_targets/quadratic.rs
index f7afb1b0..4fcbe47a 100644
--- a/fuzz/fuzz_targets/quadratic.rs
+++ b/fuzz/fuzz_targets/quadratic.rs
@@ -196,6 +196,8 @@ struct FuzzExtensionOptions {
math_dollars: bool,
math_code: bool,
shortcodes: bool,
+ wikilinks_title_after_pipe: bool,
+ wikilinks_title_before_pipe: bool,
}
impl FuzzExtensionOptions {
@@ -213,6 +215,8 @@ impl FuzzExtensionOptions {
extension.math_dollars = self.math_dollars;
extension.math_code = self.math_code;
extension.shortcodes = self.shortcodes;
+ extension.wikilinks_title_after_pipe = self.wikilinks_title_after_pipe;
+ extension.wikilinks_title_before_pipe = self.wikilinks_title_before_pipe;
extension.front_matter_delimiter = None;
extension.header_ids = None;
extension
diff --git a/script/cibuild b/script/cibuild
index 503c7124..2df91085 100755
--- a/script/cibuild
+++ b/script/cibuild
@@ -40,6 +40,10 @@ python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/math_dol
|| failed=1
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/math_code.md "$PROGRAM_ARG -e math-code" \
|| failed=1
+python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/wikilinks_title_after_pipe.md "$PROGRAM_ARG -e wikilinks-title-after-pipe" \
+ || failed=1
+python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/wikilinks_title_before_pipe.md "$PROGRAM_ARG -e wikilinks-title-before-pipe" \
+ || failed=1
python3 spec_tests.py --no-normalize --spec regression.txt "$PROGRAM_ARG" \
|| failed=1
diff --git a/src/cm.rs b/src/cm.rs
index 192d2aa2..9b8ba51b 100644
--- a/src/cm.rs
+++ b/src/cm.rs
@@ -2,7 +2,7 @@ use crate::ctype::{isalpha, isdigit, ispunct, isspace};
use crate::nodes::TableAlignment;
use crate::nodes::{
AstNode, ListDelimType, ListType, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink,
- NodeMath, NodeTable, NodeValue,
+ NodeMath, NodeTable, NodeValue, NodeWikiLink,
};
#[cfg(feature = "shortcodes")]
use crate::parser::shortcodes::NodeShortCode;
@@ -385,6 +385,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
// noop - automatic escaping is already being done
}
NodeValue::Math(ref math) => self.format_math(math, allow_wrap, entering),
+ NodeValue::WikiLink(ref nl) => return self.format_wikilink(nl, entering),
};
true
}
@@ -689,6 +690,24 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
true
}
+ fn format_wikilink(&mut self, nl: &NodeWikiLink, entering: bool) -> bool {
+ if entering {
+ write!(self, "[[").unwrap();
+ if self.options.extension.wikilinks_title_after_pipe {
+ self.output(nl.url.as_bytes(), false, Escaping::Url);
+ write!(self, "|").unwrap();
+ }
+ } else {
+ if self.options.extension.wikilinks_title_before_pipe {
+ write!(self, "|").unwrap();
+ self.output(nl.url.as_bytes(), false, Escaping::Url);
+ }
+ write!(self, "]]").unwrap();
+ }
+
+ true
+ }
+
fn format_image(&mut self, nl: &NodeLink, allow_wrap: bool, entering: bool) {
if entering {
write!(self, "![").unwrap();
diff --git a/src/html.rs b/src/html.rs
index c78a229b..025565d2 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -1038,6 +1038,21 @@ impl<'o> HtmlFormatter<'o> {
self.render_math_inline(node, literal, display_math, dollar_math)?;
}
}
+ NodeValue::WikiLink(ref nl) => {
+ if entering {
+ self.output.write_all(b"")?;
+ } else {
+ self.output.write_all(b"")?;
+ }
+ }
}
Ok(false)
}
diff --git a/src/main.rs b/src/main.rs
index a259232b..3caebc9b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -159,6 +159,8 @@ enum Extension {
MultilineBlockQuotes,
MathDollars,
MathCode,
+ WikilinksTitleAfterPipe,
+ WikilinksTitleBeforePipe,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
@@ -238,6 +240,8 @@ 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))
.front_matter_delimiter(cli.front_matter_delimiter);
#[cfg(feature = "shortcodes")]
diff --git a/src/nodes.rs b/src/nodes.rs
index 0e073263..79bbfd7e 100644
--- a/src/nodes.rs
+++ b/src/nodes.rs
@@ -182,6 +182,9 @@ pub enum NodeValue {
/// **Inline**. A character that has been [escaped](https://github.github.com/gfm/#backslash-escapes)
Escaped,
+
+ /// **Inline**. A wikilink to some URL.
+ WikiLink(NodeWikiLink),
}
/// Alignment of a single table cell.
@@ -253,6 +256,13 @@ pub struct NodeLink {
pub title: String,
}
+/// The details of a wikilink's destination.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct NodeWikiLink {
+ /// The URL for the link destination.
+ pub url: String,
+}
+
/// The metadata of a list; the kind of list, the delimiter used and so on.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct NodeList {
@@ -489,6 +499,7 @@ impl NodeValue {
NodeValue::MultilineBlockQuote(_) => "multiline_block_quote",
NodeValue::Escaped => "escaped",
NodeValue::Math(..) => "math",
+ NodeValue::WikiLink(..) => "wikilink",
}
}
}
@@ -639,7 +650,8 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
| NodeValue::Emph
| NodeValue::Strong
| NodeValue::Link(..)
- | NodeValue::Image(..) => !child.block(),
+ | NodeValue::Image(..)
+ | NodeValue::WikiLink(..) => !child.block(),
NodeValue::Table(..) => matches!(*child, NodeValue::TableRow(..)),
@@ -657,6 +669,7 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
| NodeValue::Strikethrough
| NodeValue::HtmlInline(..)
| NodeValue::Math(..)
+ | NodeValue::WikiLink(..)
),
#[cfg(feature = "shortcodes")]
@@ -672,6 +685,7 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
| NodeValue::Strikethrough
| NodeValue::HtmlInline(..)
| NodeValue::Math(..)
+ | NodeValue::WikiLink(..)
),
NodeValue::MultilineBlockQuote(_) => {
diff --git a/src/parser/inlines.rs b/src/parser/inlines.rs
index c582b309..92c8a6ab 100644
--- a/src/parser/inlines.rs
+++ b/src/parser/inlines.rs
@@ -2,7 +2,8 @@ use crate::arena_tree::Node;
use crate::ctype::{isdigit, ispunct, isspace};
use crate::entity;
use crate::nodes::{
- Ast, AstNode, NodeCode, NodeFootnoteReference, NodeLink, NodeMath, NodeValue, Sourcepos,
+ Ast, AstNode, NodeCode, NodeFootnoteReference, NodeLink, NodeMath, NodeValue, NodeWikiLink,
+ Sourcepos,
};
#[cfg(feature = "shortcodes")]
use crate::parser::shortcodes::NodeShortCode;
@@ -105,6 +106,12 @@ struct Bracket<'a> {
bracket_after: bool,
}
+#[derive(Clone, Copy)]
+struct WikilinkComponents<'i> {
+ url: &'i [u8],
+ link_label: Option<(&'i [u8], usize, usize)>,
+}
+
impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
pub fn new(
arena: &'a Arena>,
@@ -183,11 +190,30 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
'.' => Some(self.handle_period()),
'[' => {
self.pos += 1;
- let inl =
- self.make_inline(NodeValue::Text("[".to_string()), self.pos - 1, self.pos - 1);
- self.push_bracket(false, inl);
- self.within_brackets = true;
- Some(inl)
+
+ let mut wikilink_inl = None;
+
+ if (self.options.extension.wikilinks_title_after_pipe
+ || self.options.extension.wikilinks_title_before_pipe)
+ && !self.within_brackets
+ && self.peek_char() == Some(&(b'['))
+ {
+ wikilink_inl = self.handle_wikilink();
+ }
+
+ if wikilink_inl.is_none() {
+ let inl = self.make_inline(
+ NodeValue::Text("[".to_string()),
+ self.pos - 1,
+ self.pos - 1,
+ );
+ self.push_bracket(false, inl);
+ self.within_brackets = true;
+
+ Some(inl)
+ } else {
+ wikilink_inl
+ }
}
']' => {
self.within_brackets = false;
@@ -1548,6 +1574,127 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
}
}
+ // Handles wikilink syntax
+ // [[link text|url]]
+ // [[url|link text]]
+ pub fn handle_wikilink(&mut self) -> Option<&'a AstNode<'a>> {
+ let startpos = self.pos;
+ let component = self.wikilink_url_link_label()?;
+ let url_clean = strings::clean_url(component.url);
+ let (link_label, link_label_start_column, link_label_end_column) =
+ match component.link_label {
+ Some((label, sc, ec)) => (entity::unescape_html(label), sc, ec),
+ None => (
+ entity::unescape_html(component.url),
+ startpos + 1,
+ self.pos - 3,
+ ),
+ };
+
+ let nl = NodeWikiLink {
+ url: String::from_utf8(url_clean).unwrap(),
+ };
+ let inl = self.make_inline(NodeValue::WikiLink(nl), startpos - 1, self.pos - 1);
+ inl.append(self.make_inline(
+ NodeValue::Text(String::from_utf8(link_label).unwrap()),
+ link_label_start_column,
+ link_label_end_column,
+ ));
+
+ Some(inl)
+ }
+
+ fn wikilink_url_link_label(&mut self) -> Option> {
+ let left_startpos = self.pos;
+
+ if self.peek_char() != Some(&(b'[')) {
+ return None;
+ }
+
+ let found_left = self.wikilink_component();
+
+ if !found_left {
+ self.pos = left_startpos;
+ return None;
+ }
+
+ let left = strings::trim_slice(&self.input[left_startpos + 1..self.pos]);
+
+ if self.peek_char() == Some(&(b']')) && self.peek_char_n(1) == Some(&(b']')) {
+ self.pos += 2;
+ return Some(WikilinkComponents {
+ url: left,
+ link_label: None,
+ });
+ } else if self.peek_char() != Some(&(b'|')) {
+ self.pos = left_startpos;
+ return None;
+ }
+
+ let right_startpos = self.pos;
+ let found_right = self.wikilink_component();
+
+ if !found_right {
+ self.pos = left_startpos;
+ return None;
+ }
+
+ let right = strings::trim_slice(&self.input[right_startpos + 1..self.pos]);
+
+ 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 {
+ url: left,
+ link_label: Some((right, right_startpos + 1, self.pos - 3)),
+ })
+ } else {
+ Some(WikilinkComponents {
+ url: right,
+ link_label: Some((left, left_startpos + 1, right_startpos - 1)),
+ })
+ }
+ } else {
+ self.pos = left_startpos;
+ None
+ }
+ }
+
+ // Locates the edge of a wikilink component (link label or url), and sets the
+ // self.pos to it's end if it's found.
+ fn wikilink_component(&mut self) -> bool {
+ let startpos = self.pos;
+
+ if self.peek_char() != Some(&(b'[')) && self.peek_char() != Some(&(b'|')) {
+ return false;
+ }
+
+ self.pos += 1;
+
+ let mut length = 0;
+ let mut c = 0;
+ while unwrap_into_copy(self.peek_char(), &mut c) && c != b'[' && c != b']' && c != b'|' {
+ if c == b'\\' {
+ self.pos += 1;
+ length += 1;
+ if self.peek_char().map_or(false, |&c| ispunct(c)) {
+ self.pos += 1;
+ length += 1;
+ }
+ } else {
+ self.pos += 1;
+ length += 1;
+ }
+ if length > MAX_LINK_LABEL_LENGTH {
+ self.pos = startpos;
+ return false;
+ }
+ }
+
+ true
+ }
+
pub fn spnl(&mut self) {
self.skip_spaces();
if self.skip_line_end() {
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index c5c10f4c..bddd69f5 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -423,6 +423,36 @@ pub struct ExtensionOptions {
/// "Happy Friday! 😄
\n");
/// ```
pub shortcodes: bool,
+
+ /// Enables wikilinks using title after pipe syntax
+ ///
+ /// ```` md
+ /// [[url|link label]]
+ /// ````
+ ///
+ /// ```
+ /// # 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");
+ /// ```
+ pub wikilinks_title_after_pipe: bool,
+
+ /// Enables wikilinks using title before pipe syntax
+ ///
+ /// ```` md
+ /// [[link label|url]]
+ /// ````
+ ///
+ /// ```
+ /// # use comrak::{markdown_to_html, Options};
+ /// let mut options = Options::default();
+ /// options.extension.wikilinks_title_before_pipe = true;
+ /// assert_eq!(markdown_to_html("[[link label|url]]", &options),
+ /// "link label
\n");
+ /// ```
+ pub wikilinks_title_before_pipe: bool,
}
#[non_exhaustive]
diff --git a/src/tests.rs b/src/tests.rs
index a3467bf9..986cc83c 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -25,6 +25,7 @@ mod superscript;
mod table;
mod tagfilter;
mod tasklist;
+mod wikilinks;
mod xml;
#[track_caller]
@@ -141,6 +142,8 @@ macro_rules! html_opts {
math_code: true,
front_matter_delimiter: Some("---".to_string()),
shortcodes: true,
+ wikilinks_title_after_pipe: true,
+ wikilinks_title_before_pipe: true,
},
parse: $crate::ParseOptions {
smart: true,
diff --git a/src/tests/api.rs b/src/tests/api.rs
index f89125b7..16adef8d 100644
--- a/src/tests/api.rs
+++ b/src/tests/api.rs
@@ -50,6 +50,8 @@ fn exercise_full_api() {
extension.front_matter_delimiter(None);
#[cfg(feature = "shortcodes")]
extension.shortcodes(true);
+ extension.wikilinks_title_after_pipe(true);
+ extension.wikilinks_title_before_pipe(true);
let mut parse = ParseOptionsBuilder::default();
parse.smart(false);
@@ -226,5 +228,8 @@ fn exercise_full_api() {
let _: bool = math.dollar_math;
let _: String = math.literal;
}
+ nodes::NodeValue::WikiLink(nl) => {
+ let _: String = nl.url;
+ }
}
}
diff --git a/src/tests/commonmark.rs b/src/tests/commonmark.rs
index 3f297e02..6f2f5a48 100644
--- a/src/tests/commonmark.rs
+++ b/src/tests/commonmark.rs
@@ -55,3 +55,12 @@ fn math(markdown: &str, cm: &str) {
commonmark(markdown, cm, Some(&options));
}
+
+#[test_case("This [[url]] that", "This [[url|url]] that\n")]
+#[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;
+
+ commonmark(markdown, cm, Some(&options));
+}
diff --git a/src/tests/fixtures/wikilinks_title_after_pipe.md b/src/tests/fixtures/wikilinks_title_after_pipe.md
new file mode 100644
index 00000000..7c645ccf
--- /dev/null
+++ b/src/tests/fixtures/wikilinks_title_after_pipe.md
@@ -0,0 +1,47 @@
+---
+title: Wikilinks
+based_on: https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/wikilinks_title_after_pipe.md
+---
+
+# Wikilinks, title after pipe
+
+Wikilinks can have one of the following forms:
+
+ [[https://example.org]]
+ [[https://example.org|title]]
+ [[name of page]]
+ [[name of page|title]]
+
+With this version of wikilinks, the title comes after the pipe.
+
+```````````````````````````````` example
+[[https://example.org]]
+.
+https://example.org
+````````````````````````````````
+
+```````````````````````````````` example
+[[https://example.org|title]]
+.
+title
+````````````````````````````````
+
+```````````````````````````````` example
+[[Name of page]]
+.
+Name of page
+````````````````````````````````
+
+```````````````````````````````` example
+[[Name of page|Title]]
+.
+Title
+````````````````````````````````
+
+HTML entities are recognized both in the name of page and in the link title.
+
+```````````````````````````````` example
+[[Geschütztes Leerzeichen|Über ]]
+.
+Ãœber
+````````````````````````````````
\ No newline at end of file
diff --git a/src/tests/fixtures/wikilinks_title_before_pipe.md b/src/tests/fixtures/wikilinks_title_before_pipe.md
new file mode 100644
index 00000000..e430380c
--- /dev/null
+++ b/src/tests/fixtures/wikilinks_title_before_pipe.md
@@ -0,0 +1,55 @@
+---
+title: Wikilinks
+based_on: https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/wikilinks_title_before_pipe.md
+---
+
+# Wikilinks, title before pipe
+
+Wikilinks can have one of the following forms:
+
+ [[https://example.org]]
+ [[title|https://example.org]]
+ [[name of page]]
+ [[title|name of page]]
+
+With this version of wikilinks, the title comes before the pipe.
+
+```````````````````````````````` example
+[[https://example.org]]
+.
+https://example.org
+````````````````````````````````
+
+```````````````````````````````` example
+[[title|https://example.org]]
+.
+title
+````````````````````````````````
+
+```````````````````````````````` example
+[[Name of page]]
+.
+Name of page
+````````````````````````````````
+
+```````````````````````````````` example
+[[Title|Name of page]]
+.
+Title
+````````````````````````````````
+
+Regular links should still work!
+
+```````````````````````````````` example
+[Title](Name%20of%20page)
+.
+Title
+````````````````````````````````
+
+HTML entities are recognized both in the name of page and in the link title.
+
+```````````````````````````````` example
+[[Über |Geschütztes Leerzeichen]]
+.
+Ãœber
+````````````````````````````````
\ No newline at end of file
diff --git a/src/tests/wikilinks.rs b/src/tests/wikilinks.rs
new file mode 100644
index 00000000..6b0c94c3
--- /dev/null
+++ b/src/tests/wikilinks.rs
@@ -0,0 +1,213 @@
+use super::*;
+
+// html_opts! does a roundtrip check unless sourcepos is set.
+// These cases don't work roundtrip, because converting to commonmark
+// automatically escapes certain characters.
+#[test]
+fn wikilinks_does_not_unescape_html_entities_in_link_label() {
+ html_opts!(
+ [extension.wikilinks_title_after_pipe, render.sourcepos],
+ concat!("This is [[<script>alert(0)</script>|a <link]]",),
+ concat!("This is a <link
\n"),
+ );
+
+ html_opts!(
+ [extension.wikilinks_title_before_pipe, render.sourcepos],
+ concat!("This is [[a <link|<script>alert(0)</script>]]",),
+ concat!("This is a <link
\n"),
+ );
+}
+
+#[test]
+fn wikilinks_sanitizes_the_href_attribute_case_1() {
+ html_opts!(
+ [extension.wikilinks_title_after_pipe],
+ concat!("[[http:\'\"injected=attribute><img/src=\"0\"onerror=\"alert(0)\">https://example.com|a]]",),
+ concat!("a
\n"),
+ );
+
+ html_opts!(
+ [extension.wikilinks_title_before_pipe],
+ concat!("[[a|http:\'\"injected=attribute><img/src=\"0\"onerror=\"alert(0)\">https://example.com]]",),
+ concat!("a
\n"),
+ );
+}
+
+#[test]
+fn wikilinks_sanitizes_the_href_attribute_case_2() {
+ html_opts!(
+ [extension.wikilinks_title_after_pipe],
+ 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],
+ concat!("[[a|\'\"><svg><i/class=gl-show-field-errors><input/title=\"<script>alert(0)</script>\"/></svg>https://example.com]]",),
+ concat!("a
\n"),
+ );
+}
+
+#[test]
+fn wikilinks_supercedes_relaxed_autolinks() {
+ html_opts!(
+ [
+ extension.wikilinks_title_after_pipe,
+ parse.relaxed_autolinks
+ ],
+ concat!("[[http://example.com]]",),
+ concat!(
+ "http://example.com
\n"
+ ),
+ );
+
+ html_opts!(
+ [
+ extension.wikilinks_title_before_pipe,
+ parse.relaxed_autolinks
+ ],
+ concat!("[[http://example.com]]",),
+ concat!(
+ "http://example.com
\n"
+ ),
+ );
+}
+
+#[test]
+fn wikilinks_only_url_in_tables() {
+ html_opts!(
+ [extension.wikilinks_title_after_pipe, extension.table],
+ concat!("| header |\n", "| ------- |\n", "| [[url]] |\n",),
+ concat!(
+ "\n",
+ "\n",
+ "\n",
+ "header | \n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "url | \n",
+ "
\n",
+ "\n",
+ "
\n",
+ ),
+ );
+
+ html_opts!(
+ [extension.wikilinks_title_before_pipe, extension.table],
+ concat!("| header |\n", "| ------- |\n", "| [[url]] |\n",),
+ concat!(
+ "\n",
+ "\n",
+ "\n",
+ "header | \n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "url | \n",
+ "
\n",
+ "\n",
+ "
\n",
+ ),
+ );
+}
+
+#[test]
+fn wikilinks_full_in_tables_not_supported() {
+ html_opts!(
+ [extension.wikilinks_title_after_pipe, extension.table],
+ concat!("| header |\n", "| ------- |\n", "| [[url|link label]] |\n",),
+ concat!(
+ "\n",
+ "\n",
+ "\n",
+ "header | \n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "[[url | \n",
+ "
\n",
+ "\n",
+ "
\n",
+ ),
+ );
+
+ html_opts!(
+ [extension.wikilinks_title_before_pipe, extension.table],
+ concat!("| header |\n", "| ------- |\n", "| [[link label|url]] |\n",),
+ concat!(
+ "\n",
+ "\n",
+ "\n",
+ "header | \n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "[[link label | \n",
+ "
\n",
+ "\n",
+ "
\n",
+ ),
+ );
+}
+
+#[test]
+fn wikilinks_exceeds_label_limit() {
+ let long_label = format!("[[{:b<1100}]]", "a");
+ let expected = format!("{}
\n", long_label);
+
+ html_opts!(
+ [extension.wikilinks_title_after_pipe],
+ &long_label,
+ &expected,
+ );
+}
+
+#[test]
+fn sourcepos() {
+ assert_ast_match!(
+ [extension.wikilinks_title_after_pipe],
+ "This [[http://example.com|link label]] that\n",
+ (document (1:1-1:43) [
+ (paragraph (1:1-1:43) [
+ (text (1:1-1:5) "This ")
+ (wikilink (1:6-1:38) [
+ (text (1:27-1:36) "link label")
+ ])
+ (text (1:39-1:43) " that")
+ ])
+ ])
+ );
+
+ assert_ast_match!(
+ [extension.wikilinks_title_before_pipe],
+ "This [[link label|http://example.com]] that\n",
+ (document (1:1-1:43) [
+ (paragraph (1:1-1:43) [
+ (text (1:1-1:5) "This ")
+ (wikilink (1:6-1:38) [
+ (text (1:8-1:17) "link label")
+ ])
+ (text (1:39-1:43) " that")
+ ])
+ ])
+ );
+
+ assert_ast_match!(
+ [extension.wikilinks_title_before_pipe],
+ "This [[http://example.com]] that\n",
+ (document (1:1-1:32) [
+ (paragraph (1:1-1:32) [
+ (text (1:1-1:5) "This ")
+ (wikilink (1:6-1:27) [
+ (text (1:8-1:25) "http://example.com")
+ ])
+ (text (1:28-1:32) " that")
+ ])
+ ])
+ );
+}
diff --git a/src/xml.rs b/src/xml.rs
index dd90ecf0..3d2e3ebd 100644
--- a/src/xml.rs
+++ b/src/xml.rs
@@ -273,6 +273,11 @@ impl<'o> XmlFormatter<'o> {
write!(self.output, "{}", ast.value.xml_node_name())?;
was_literal = true;
}
+ NodeValue::WikiLink(ref nl) => {
+ self.output.write_all(b" destination=\"")?;
+ self.escape(nl.url.as_bytes())?;
+ self.output.write_all(b"\"")?;
+ }
}
if node.first_child().is_some() {