Skip to content

Commit

Permalink
Add support for wikilinks syntax
Browse files Browse the repository at this point in the history
supporting either

[[url|link text]]

or

[[link text|url]]
  • Loading branch information
digitalmoksha committed May 10, 2024
1 parent cf9d263 commit ec57746
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 6 deletions.
2 changes: 2 additions & 0 deletions examples/s-expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 3 additions & 1 deletion fuzz/fuzz_targets/all_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
4 changes: 4 additions & 0 deletions fuzz/fuzz_targets/quadratic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions script/cibuild
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ if [ x"$SPEC" = "xtrue" ]; then
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/math_dollars.md "$PROGRAM_ARG -e math-dollars" \
|| 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" \
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ enum Extension {
MultilineBlockQuotes,
MathDollars,
MathCode,
WikilinksTitleAfterPipe,
WikilinksTitleBeforePipe,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
Expand Down Expand Up @@ -239,6 +241,8 @@ fn main() -> Result<(), Box<dyn Error>> {
.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")]
Expand Down
140 changes: 135 additions & 5 deletions src/parser/inlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,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;
Expand Down Expand Up @@ -1548,6 +1567,117 @@ 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 (url, title) = self.wikilink_url_title();

url?;

let url_clean = strings::clean_url(url.unwrap());
let title_clean = match title {
Some(title) => entity::unescape_html(title),
None => entity::unescape_html(url.unwrap()),
};

let nl = NodeLink {
url: String::from_utf8(url_clean).unwrap(),
title: String::new(),
};
let inl = self.make_inline(NodeValue::Link(nl), startpos - 1, self.pos - 1);
inl.append(self.make_inline(
NodeValue::Text(String::from_utf8(title_clean).unwrap()),
startpos - 1,
self.pos - 1,
));

Some(inl)
}

pub fn wikilink_url_title(&mut self) -> (Option<&[u8]>, Option<&[u8]>) {
let left_startpos = self.pos;

if self.peek_char() != Some(&(b'[')) {
return (None, None);
}

let found_left = self.wikilink_component();

if !found_left {
self.pos = left_startpos;
return (None, 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(left), None);
} else if self.peek_char() != Some(&(b'|')) {
self.pos = left_startpos;
return (None, None);
}

let right_startpos = self.pos;
let found_right = self.wikilink_component();

if !found_right {
self.pos = left_startpos;
return (None, 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(left), Some(right))
} else {
(Some(right), Some(left))
}
} else {
self.pos = left_startpos;
(None, None)
}
}

// Locates the edge of a wikilink component (link text or url), and sets the
// self.pos to it's end if it's found.
pub 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() {
Expand Down
30 changes: 30 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,36 @@ pub struct ExtensionOptions {
/// "<p>Happy Friday! 😄</p>\n");
/// ```
pub shortcodes: bool,

/// Enables wikilinks using title after pipe syntax
///
/// ```` md
/// [[url|link text]]
/// ````
///
/// ```
/// # 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 text]]", &options),
/// "<p><a href="url">link text</a></p>\n");
/// ```
pub wikilinks_title_after_pipe: bool,

/// Enables wikilinks using title before pipe syntax
///
/// ```` md
/// [[link text|url]]
/// ````
///
/// ```
/// # use comrak::{markdown_to_html, Options};
/// let mut options = Options::default();
/// options.extension.wikilinks_title_after_pipe = true;
/// assert_eq!(markdown_to_html("[[link text|url]]", &options),
/// "<p><a href="url">link text</a></p>\n");
/// ```
pub wikilinks_title_before_pipe: bool,
}

#[non_exhaustive]
Expand Down
3 changes: 3 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod superscript;
mod table;
mod tagfilter;
mod tasklist;
mod wikilinks;
mod xml;

#[track_caller]
Expand Down Expand Up @@ -142,6 +143,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,
Expand Down
2 changes: 2 additions & 0 deletions src/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
47 changes: 47 additions & 0 deletions src/tests/fixtures/wikilinks_title_after_pipe.md
Original file line number Diff line number Diff line change
@@ -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]]
.
<p><a href="https://example.org">https://example.org</a></p>
````````````````````````````````

```````````````````````````````` example
[[https://example.org|title]]
.
<p><a href="https://example.org">title</a></p>
````````````````````````````````

```````````````````````````````` example
[[Name of page]]
.
<p><a href="Name%20of%20page">Name of page</a></p>
````````````````````````````````

```````````````````````````````` example
[[Name of page|Title]]
.
<p><a href="Name%20of%20page">Title</a></p>
````````````````````````````````

HTML entities are recognized both in the name of page and in the link title.

```````````````````````````````` example
[[Gesch&uuml;tztes Leerzeichen|&#xDC;ber &amp;nbsp;]]
.
<p><a href="Gesch%C3%BCtztes%20Leerzeichen">Über &amp;nbsp;</a></p>
````````````````````````````````
55 changes: 55 additions & 0 deletions src/tests/fixtures/wikilinks_title_before_pipe.md
Original file line number Diff line number Diff line change
@@ -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]]
.
<p><a href="https://example.org">https://example.org</a></p>
````````````````````````````````

```````````````````````````````` example
[[title|https://example.org]]
.
<p><a href="https://example.org">title</a></p>
````````````````````````````````

```````````````````````````````` example
[[Name of page]]
.
<p><a href="Name%20of%20page">Name of page</a></p>
````````````````````````````````

```````````````````````````````` example
[[Title|Name of page]]
.
<p><a href="Name%20of%20page">Title</a></p>
````````````````````````````````

Regular links should still work!

```````````````````````````````` example
[Title](Name%20of%20page)
.
<p><a href="Name%20of%20page">Title</a></p>
````````````````````````````````

HTML entities are recognized both in the name of page and in the link title.

```````````````````````````````` example
[[&#xDC;ber &amp;nbsp;|Gesch&uuml;tztes Leerzeichen]]
.
<p><a href="Gesch%C3%BCtztes%20Leerzeichen">Über &amp;nbsp;</a></p>
````````````````````````````````
2 changes: 2 additions & 0 deletions src/tests/propfuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ fn propfuzz_doesnt_crash(md: String) {
front_matter_delimiter: None,
#[cfg(feature = "shortcodes")]
shortcodes: true,
wikilinks_title_after_pipe: true,
wikilinks_title_before_pipe: true,
},
parse: ParseOptions {
smart: true,
Expand Down
Loading

0 comments on commit ec57746

Please sign in to comment.