#[derive(Debug, PartialEq, Eq)]
pub enum Context<'a> {
Path {
partial: &'a str,
},
Fragment {
target: &'a str,
partial: &'a str,
},
Predicate {
target: &'a str,
partial: &'a str,
},
ReferenceLabel {
partial: &'a str,
},
Footnote {
partial: &'a str,
},
}
#[must_use]
pub fn detect(prefix: &str) -> Option<Context<'_>> {
detect_destination(prefix).or_else(|| detect_bracket(prefix))
}
fn detect_destination(prefix: &str) -> Option<Context<'_>> {
let open = prefix.rfind("](")? + 2;
let dest = &prefix[open..];
if dest.contains(')') {
return None;
}
if let Some(quote) = dest.find('"') {
let after_quote = &dest[quote + 1..];
if after_quote.contains('"') {
return None;
}
return Some(Context::Predicate {
target: dest[..quote].trim(),
partial: after_quote,
});
}
if dest.contains(char::is_whitespace) {
return None;
}
if let Some(hash) = dest.find('#') {
return Some(Context::Fragment {
target: &dest[..hash],
partial: &dest[hash + 1..],
});
}
Some(Context::Path { partial: dest })
}
fn detect_bracket(prefix: &str) -> Option<Context<'_>> {
let open = innermost_unclosed_bracket(prefix)?;
let after = &prefix[open + 1..];
Some(
after
.strip_prefix('^')
.map_or(Context::ReferenceLabel { partial: after }, |label| {
Context::Footnote { partial: label }
}),
)
}
fn innermost_unclosed_bracket(prefix: &str) -> Option<usize> {
let mut stack: Vec<usize> = Vec::new();
for (i, b) in prefix.bytes().enumerate() {
match b {
b'[' => stack.push(i),
b']' => {
stack.pop();
}
_ => {}
}
}
stack.pop()
}
#[cfg(test)]
mod tests {
use super::{Context, detect};
#[test]
fn prose_has_no_context() {
assert_eq!(detect(""), None, "empty prefix is not a completion site");
assert_eq!(
detect("just some prose here"),
None,
"plain prose is not a completion site"
);
}
#[test]
fn empty_destination_is_path() {
assert_eq!(
detect("[link]("),
Some(Context::Path { partial: "" }),
"an open destination with no text completes paths"
);
}
#[test]
fn partial_destination_is_path() {
assert_eq!(
detect("[link](./docs/gui"),
Some(Context::Path {
partial: "./docs/gui"
}),
"destination text is reported whole, directories included"
);
}
#[test]
fn image_destination_is_path() {
assert_eq!(
detect(",
Some(Context::Path { partial: "img/lo" }),
"image destinations complete paths like link destinations"
);
}
#[test]
fn closed_destination_is_not_a_context() {
assert_eq!(
detect("[link](done.md)"),
None,
"a closed `)` ends the destination context"
);
assert_eq!(
detect("[link](done.md) and more prose"),
None,
"text after a closed link is prose"
);
}
#[test]
fn hash_in_destination_is_fragment() {
assert_eq!(
detect("[link](other.md#"),
Some(Context::Fragment {
target: "other.md",
partial: ""
}),
"`#` after a path opens a fragment against that target"
);
assert_eq!(
detect("[link](other.md#sec"),
Some(Context::Fragment {
target: "other.md",
partial: "sec"
}),
"fragment partial is the text after `#`"
);
}
#[test]
fn in_doc_hash_is_current_document_fragment() {
assert_eq!(
detect("[link](#"),
Some(Context::Fragment {
target: "",
partial: ""
}),
"`(#` targets the current document (empty target)"
);
assert_eq!(
detect("[link](#cont"),
Some(Context::Fragment {
target: "",
partial: "cont"
}),
"in-doc fragment carries its partial"
);
}
#[test]
fn open_title_is_predicate() {
assert_eq!(
detect("[link](target.md \""),
Some(Context::Predicate {
target: "target.md",
partial: ""
}),
"an open title quote opens predicate completion"
);
assert_eq!(
detect("[link](target.md \"sup"),
Some(Context::Predicate {
target: "target.md",
partial: "sup"
}),
"predicate partial is the text after the opening quote, target carried"
);
}
#[test]
fn closed_title_is_not_a_context() {
assert_eq!(
detect("[link](target.md \"supersedes\""),
None,
"a closed title quote ends the predicate context"
);
}
#[test]
fn unquoted_whitespace_after_url_is_not_a_context() {
assert_eq!(
detect("[link](target.md "),
None,
"whitespace with no opening quote is the un-completable title area"
);
}
#[test]
fn full_reference_label() {
assert_eq!(
detect("[text]["),
Some(Context::ReferenceLabel { partial: "" }),
"the second bracket of a full reference completes labels"
);
assert_eq!(
detect("[text][la"),
Some(Context::ReferenceLabel { partial: "la" }),
"reference label partial is the text after `][`"
);
}
#[test]
fn shortcut_reference_label() {
assert_eq!(
detect("[la"),
Some(Context::ReferenceLabel { partial: "la" }),
"a lone open bracket completes shortcut reference labels"
);
}
#[test]
fn footnote_label() {
assert_eq!(
detect("[^"),
Some(Context::Footnote { partial: "" }),
"`[^` opens footnote completion"
);
assert_eq!(
detect("text[^no"),
Some(Context::Footnote { partial: "no" }),
"footnote partial is the text after `^`"
);
}
#[test]
fn destination_wins_over_inner_bracket() {
assert_eq!(
detect("[outer [inner]("),
Some(Context::Path { partial: "" }),
"an open destination is matched before any unclosed bracket"
);
}
#[test]
fn reference_after_closed_link() {
assert_eq!(
detect("[a](b.md) then [re"),
Some(Context::ReferenceLabel { partial: "re" }),
"a fresh open bracket after a closed link completes labels"
);
}
}