use super::Converter;
use super::line::matching_bracket;
use super::params::{self, interpolation_at, is_space, name_len, string_parts};
use super::source::right;
use crate::diagnostic::Code;
use crate::tree::{
Image, ImageReference, Interpolation, Leaf, Link, LinkReference, Literal, Node, Parent, Point,
Position, Reference, ReferenceType, StringPart, Void, WikiLink,
};
use markdown::mdast;
use std::iter::Peekable;
impl Converter<'_, '_> {
pub(super) fn inlines(&mut self, nodes: Vec<mdast::Node>, in_table: bool) -> Vec<Node> {
let mut out = Vec::new();
let mut nodes = nodes.into_iter().peekable();
while let Some(node) = nodes.next() {
match node {
mdast::Node::CompmdRaw(raw) => self.raw_inline(raw, &mut nodes, in_table, &mut out),
other => out.extend(self.inline(other, in_table)),
}
}
merge_texts(out)
}
pub(super) fn inline(&mut self, node: mdast::Node, in_table: bool) -> Option<Node> {
let parent =
|this: &mut Self, children, position: Option<markdown::unist::Position>| Parent {
children: this.inlines(children, in_table),
position: this.source.position(position.as_ref()),
};
Some(match node {
mdast::Node::Text(text) => Node::Text(self.literal(text.value, text.position.as_ref())),
mdast::Node::InlineCode(code) => {
Node::InlineCode(self.literal(code.value, code.position.as_ref()))
}
mdast::Node::InlineMath(math) => {
Node::InlineMath(self.literal(math.value, math.position.as_ref()))
}
mdast::Node::Html(html) => Node::Html(self.literal(html.value, html.position.as_ref())),
mdast::Node::Emphasis(node) => {
Node::Emphasis(parent(self, node.children, node.position))
}
mdast::Node::Strong(node) => Node::Strong(parent(self, node.children, node.position)),
mdast::Node::Delete(node) => Node::Delete(parent(self, node.children, node.position)),
mdast::Node::Break(node) => Node::Break(Void {
position: self.source.position(node.position.as_ref()),
}),
mdast::Node::Link(link) => Node::Link(Link {
children: self.inlines(link.children, in_table),
url_parts: self.url_parts(&link.url),
url: link.url,
title: link.title,
position: self.source.position(link.position.as_ref()),
}),
mdast::Node::Image(image) => Node::Image(Image {
alt: image.alt,
url_parts: self.url_parts(&image.url),
url: image.url,
title: image.title,
position: self.source.position(image.position.as_ref()),
}),
mdast::Node::LinkReference(link) => Node::LinkReference(LinkReference {
children: self.inlines(link.children, in_table),
reference_type: reference_type(link.reference_kind),
identifier: link.identifier,
label: link.label,
position: self.source.position(link.position.as_ref()),
}),
mdast::Node::ImageReference(image) => Node::ImageReference(ImageReference {
alt: image.alt,
reference_type: reference_type(image.reference_kind),
identifier: image.identifier,
label: image.label,
position: self.source.position(image.position.as_ref()),
}),
mdast::Node::FootnoteReference(footnote) => Node::FootnoteReference(Reference {
identifier: footnote.identifier,
label: footnote.label,
position: self.source.position(footnote.position.as_ref()),
}),
_ => return None,
})
}
fn literal(&self, value: String, position: Option<&markdown::unist::Position>) -> Literal {
Literal {
value,
position: self.source.position(position),
}
}
fn raw_inline(
&mut self,
raw: mdast::CompmdRaw,
next: &mut Peekable<impl Iterator<Item = mdast::Node>>,
in_table: bool,
out: &mut Vec<Node>,
) {
let position = self.source.position(raw.position.as_ref());
let start = position.map_or(super::START, |position| position.start);
match raw.kind {
mdast::CompmdRawKind::ComponentInline => {
out.extend(self.component(&raw.value, position, next.peek(), in_table));
}
mdast::CompmdRawKind::Interpolation => {
let chars: Vec<char> = raw.value.chars().collect();
let path = interpolation_at(&chars, 0)
.map(|(path, _)| path)
.unwrap_or_default();
out.push(Node::Interpolation(Interpolation {
path,
block: false,
position,
}));
}
mdast::CompmdRawKind::InterpolationLiteral => {
self.diagnose(
Code::W106,
start,
"`{{` isn't followed by a path, so it is text; write `\\{{` for literal braces, and use `:value` for a computed value",
);
out.push(Node::Text(Literal {
value: raw.value,
position,
}));
}
mdast::CompmdRawKind::WikiLink => {
out.push(Node::WikiLink(wiki_link(&raw.value, position)));
}
mdast::CompmdRawKind::ComponentLine => out.push(Node::Text(Literal {
value: raw.value,
position,
})),
}
}
fn component(
&mut self,
raw: &str,
position: Option<Position>,
next: Option<&mdast::Node>,
in_table: bool,
) -> Vec<Node> {
let text: Vec<char> = raw.chars().collect();
let start = position.map_or(super::START, |position| position.start);
let name_end = 1 + name_len(&text, 1).unwrap_or(0);
let close = matching_bracket(&text, name_end).unwrap_or(text.len().saturating_sub(1));
let fallback_text: String = text[name_end + 1..close].iter().collect();
let mut component = Leaf {
name: text[1..name_end].iter().collect(),
fallback: self.fallback(&fallback_text, right(start, name_end + 1)),
params: Vec::new(),
position,
recovered: false,
};
if close + 1 < text.len() {
let (chars, indexes) = unescape_pipes(&text[close + 1..], in_table);
let place = |index: usize| right(start, close + 1 + indexes[index]);
let list = params::braced(&chars, 0);
if let Some(at) = list.broke_at {
self.diagnose(Code::F011, place(at), BROKEN_INLINE);
component.recovered = true;
let rest = Literal {
value: text[close + 1..].iter().collect(),
position: position.map(|position| Position {
start: right(start, close + 1),
end: position.end,
}),
};
return vec![Node::ComponentInline(component), Node::Text(rest)];
}
self.add_params_at(&mut component.params, list, place);
} else if let Some(mdast::Node::Text(after)) = next {
self.params_after(&mut component, after);
}
vec![Node::ComponentInline(component)]
}
fn params_after(&mut self, component: &mut Leaf, after: &mdast::Text) {
let line: Vec<char> = after.value.lines().next().unwrap_or("").chars().collect();
let from = after.position.as_ref().map_or(super::START, |position| {
self.source.point(position.start.offset)
});
let braced_at = |at: usize| line.get(at) == Some(&'{') && line.get(at + 1) != Some(&'{');
if braced_at(0) {
let broke_at = params::braced(&line, 0).broke_at.unwrap_or(0);
self.diagnose(Code::F011, right(from, broke_at), BROKEN_INLINE);
component.recovered = true;
return;
}
let spaces = line.iter().take_while(|c| is_space(Some(c))).count();
if spaces > 0 && braced_at(spaces) && params::braced(&line, spaces).broke_at.is_none() {
self.diagnose(
Code::W107,
right(from, spaces),
format!(
"the space before `{{` makes this param list text; write `:{}[…]{{…}}` without a space",
component.name
),
);
}
}
pub(super) fn fallback(&mut self, text: &str, origin: Point) -> Vec<Node> {
let labels = self.labels;
let first = self
.syntax
.mdast(text, false, labels)
.children
.into_iter()
.next();
let Some(mdast::Node::Paragraph(paragraph)) = first else {
return Vec::new();
};
self.placed(text, origin, labels)
.inlines(paragraph.children, false)
}
pub(super) fn url_parts(&self, url: &str) -> Option<Vec<StringPart>> {
let parts = string_parts(url);
let interpolated = parts
.iter()
.any(|part| matches!(part, StringPart::Interpolation(_)));
(self.syntax.compmd && interpolated).then_some(parts)
}
}
const BROKEN_INLINE: &str = "malformed param list, so the component keeps no params and the list is text: a quote or bracket that doesn't close, a character a bare value can't hold (quote the value, or write an expression), or `=` with no value";
fn unescape_pipes(text: &[char], in_table: bool) -> (Vec<char>, Vec<usize>) {
let mut chars = Vec::new();
let mut indexes = Vec::new();
let mut at = 0;
while at < text.len() {
if in_table && text[at] == '\\' && text.get(at + 1) == Some(&'|') {
at += 1;
}
chars.push(text[at]);
indexes.push(at);
at += 1;
}
indexes.push(text.len());
(chars, indexes)
}
fn wiki_link(raw: &str, position: Option<Position>) -> WikiLink {
let inner = raw
.strip_prefix("[[")
.and_then(|r| r.strip_suffix("]]"))
.unwrap_or(raw);
let (target, label) = inner
.split_once("\\|")
.or_else(|| inner.split_once('|'))
.unwrap_or((inner, inner));
WikiLink {
target: target.to_string(),
label: label.to_string(),
position,
}
}
fn reference_type(kind: mdast::ReferenceKind) -> ReferenceType {
match kind {
mdast::ReferenceKind::Shortcut => ReferenceType::Shortcut,
mdast::ReferenceKind::Collapsed => ReferenceType::Collapsed,
mdast::ReferenceKind::Full => ReferenceType::Full,
}
}
fn merge_texts(nodes: Vec<Node>) -> Vec<Node> {
let mut out: Vec<Node> = Vec::with_capacity(nodes.len());
for node in nodes {
if let (Node::Text(text), Some(Node::Text(previous))) = (&node, out.last_mut()) {
previous.value.push_str(&text.value);
previous.position =
previous
.position
.zip(text.position)
.map(|(first, last)| Position {
start: first.start,
end: last.end,
});
continue;
}
out.push(node);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn text(value: &str) -> Node {
Node::Text(Literal {
value: value.into(),
position: None,
})
}
#[test]
fn adjacent_texts_merge() {
let merged = merge_texts(vec![
text("a"),
text("b"),
Node::Break(Void::default()),
text("c"),
]);
assert_eq!(
merged,
[text("ab"), Node::Break(Void::default()), text("c")]
);
}
#[test]
fn pipes_unescape_only_in_tables() {
let chars: Vec<char> = r"{a=(x \|\| y)}".chars().collect();
let (unescaped, indexes) = unescape_pipes(&chars, true);
assert_eq!(unescaped.iter().collect::<String>(), "{a=(x || y)}");
assert_eq!((indexes[6], indexes[7], indexes[12]), (7, 9, 14));
assert_eq!(unescape_pipes(&chars, false).0, chars);
}
#[test]
fn wiki_links_split_target_and_label() {
let link = |raw: &str| {
let link = wiki_link(raw, None);
(link.target, link.label)
};
assert_eq!(link("[[Home]]"), ("Home".into(), "Home".into()));
assert_eq!(link("[[Home|start]]"), ("Home".into(), "start".into()));
assert_eq!(link(r"[[Home\|start]]"), ("Home".into(), "start".into()));
}
}