Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for wikilinks format #407

Merged
merged 10 commits into from
May 16, 2024
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 @@ -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
Expand Down
26 changes: 25 additions & 1 deletion src/cm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,13 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
NodeValue::TaskItem(symbol) => self.format_task_item(symbol, node, entering),
NodeValue::Strikethrough => self.format_strikethrough(),
NodeValue::Superscript => self.format_superscript(),
NodeValue::Link(ref nl) => return self.format_link(node, nl, entering),
NodeValue::Link(ref nl) => {
if nl.wikilink {
return self.format_wikilink(nl, entering);
} else {
return self.format_link(node, nl, entering);
}
}
NodeValue::Image(ref nl) => self.format_image(nl, allow_wrap, entering),
#[cfg(feature = "shortcodes")]
NodeValue::ShortCode(ref ne) => self.format_shortcode(ne, entering),
Expand Down Expand Up @@ -689,6 +695,24 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
true
}

fn format_wikilink(&mut self, nl: &NodeLink, 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();
Expand Down
3 changes: 3 additions & 0 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ impl<'o> HtmlFormatter<'o> {
self.output.write_all(b"\" title=\"")?;
self.escape(nl.title.as_bytes())?;
}
if nl.wikilink {
self.output.write_all(b"\" data-wikilink=\"true")?;
}
self.output.write_all(b"\">")?;
} else {
self.output.write_all(b"</a>")?;
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ enum Extension {
MultilineBlockQuotes,
MathDollars,
MathCode,
WikilinksTitleAfterPipe,
WikilinksTitleBeforePipe,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
Expand Down Expand Up @@ -238,6 +240,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
3 changes: 3 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ pub struct NodeLink {
/// Note this field is used for the `title` attribute by the HTML formatter even for images;
/// `alt` text is supplied in the image inline text.
pub title: String,

/// Whether this is a wikilink or not
digitalmoksha marked this conversation as resolved.
Show resolved Hide resolved
pub wikilink: bool,
kivikakk marked this conversation as resolved.
Show resolved Hide resolved
}

/// The metadata of a list; the kind of list, the delimiter used and so on.
Expand Down
3 changes: 3 additions & 0 deletions src/parser/autolink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ fn www_match<'a>(
NodeValue::Link(NodeLink {
url,
title: String::new(),
wikilink: false,
}),
(0, 1, 0, 1).into(),
);
Expand Down Expand Up @@ -290,6 +291,7 @@ fn url_match<'a>(
NodeValue::Link(NodeLink {
url: url.clone(),
title: String::new(),
wikilink: false,
}),
(0, 1, 0, 1).into(),
);
Expand Down Expand Up @@ -398,6 +400,7 @@ fn email_match<'a>(
NodeValue::Link(NodeLink {
url,
title: String::new(),
wikilink: false,
}),
(0, 1, 0, 1).into(),
);
Expand Down
148 changes: 142 additions & 6 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 @@ -1472,7 +1491,11 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
pub fn close_bracket_match(&mut self, is_image: bool, url: String, title: String) {
let brackets_len = self.brackets.len();

let nl = NodeLink { url, title };
let nl = NodeLink {
url,
title,
wikilink: false,
};
let inl = self.make_inline(
if is_image {
NodeValue::Image(nl)
Expand Down Expand Up @@ -1548,6 +1571,118 @@ 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?;
kivikakk marked this conversation as resolved.
Show resolved Hide resolved

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(),
wikilink: true,
};
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]>) {
kivikakk marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -1594,6 +1729,7 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
NodeValue::Link(NodeLink {
url: String::from_utf8(strings::clean_autolink(url, kind)).unwrap(),
title: String::new(),
wikilink: false,
}),
start_column + 1,
end_column + 1,
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\" data-wikilink=\"true\">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_before_pipe = true;
/// assert_eq!(markdown_to_html("[[link text|url]]", &options),
/// "<p><a href=\"url\" data-wikilink=\"true\">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 @@ -25,6 +25,7 @@ mod superscript;
mod table;
mod tagfilter;
mod tasklist;
mod wikilinks;
mod xml;

#[track_caller]
Expand Down Expand Up @@ -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,
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
Loading