1295 lines
41 KiB
Rust
1295 lines
41 KiB
Rust
|
|
//! Vimwiki parser.
|
||
|
|
//!
|
||
|
|
//! Hand-rolled recursive-descent over `Vec<VimwikiToken>` (SPEC.md §6.7).
|
||
|
|
//! Resilient: never aborts the document. Anything the dispatcher can't
|
||
|
|
//! claim becomes a `BlockNode::Error(ErrorNode)` so progress is
|
||
|
|
//! guaranteed.
|
||
|
|
//!
|
||
|
|
//! ## Inline marker precedence
|
||
|
|
//!
|
||
|
|
//! Within a text run, the lexer emits delimiter tokens
|
||
|
|
//! (`BoldDelim` / `ItalicDelim` / `StrikethroughDelim` /
|
||
|
|
//! `SuperscriptDelim` / `SubscriptDelim`) one at a time. The parser pairs
|
||
|
|
//! them by scanning forward for the *next* token of the same kind, then
|
||
|
|
//! recursing on the slice between them:
|
||
|
|
//!
|
||
|
|
//! - `*foo*` → Bold
|
||
|
|
//! - `_foo_` → Italic
|
||
|
|
//! - `~~foo~~` → Strikethrough
|
||
|
|
//! - `^foo^` → Superscript
|
||
|
|
//! - `,,foo,,` → Subscript
|
||
|
|
//! - `_*foo*_` → Italic(Bold(...)) — semantically bold-italic
|
||
|
|
//! - `*_foo_*` → Bold(Italic(...)) — semantically bold-italic
|
||
|
|
//!
|
||
|
|
//! Unmatched delimiters fall back to literal `Text` nodes so input like
|
||
|
|
//! `2 * 3 = 6` survives without a closing pair.
|
||
|
|
//!
|
||
|
|
//! ## Link target classification
|
||
|
|
//!
|
||
|
|
//! `[[ ]]` payloads are inspected to choose between `WikiLinkNode`
|
||
|
|
//! (the default) and `ExternalLinkNode` (when the target is a URL). The
|
||
|
|
//! `LinkTarget` enum follows SPEC §9: leading `/` is root-relative,
|
||
|
|
//! `//` is filesystem-absolute, `wiki<N>:` / `wn.<Name>:` are interwiki,
|
||
|
|
//! `diary:` / `file:` / `local:` are their own kinds, and a trailing `/`
|
||
|
|
//! marks a directory link.
|
||
|
|
|
||
|
|
use crate::ast::{
|
||
|
|
BlockNode, BlockquoteNode, BoldNode, CodeNode, CommentNode, DefinitionItemNode,
|
||
|
|
DefinitionListNode, DocumentNode, ErrorNode, ExternalLinkNode, HeadingNode, HorizontalRuleNode,
|
||
|
|
InlineNode, ItalicNode, KeywordNode, LinkKind, LinkTarget, ListItemNode, ListNode, ListSymbol,
|
||
|
|
MathBlockNode, MathInlineNode, PageMetadata, ParagraphNode, PreformattedNode, RawUrlNode, Span,
|
||
|
|
StrikethroughNode, SubscriptNode, SuperscriptNode, TableCellNode, TableNode, TableRowNode,
|
||
|
|
TextNode, TransclusionNode, WikiLinkNode,
|
||
|
|
};
|
||
|
|
use crate::syntax::{Parser, TokenStream};
|
||
|
|
|
||
|
|
use super::lexer::{VimwikiToken, VimwikiTokenKind as K};
|
||
|
|
|
||
|
|
#[derive(Debug, Default, Clone)]
|
||
|
|
pub struct VimwikiParser;
|
||
|
|
|
||
|
|
impl VimwikiParser {
|
||
|
|
pub fn new() -> Self {
|
||
|
|
Self
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Parser for VimwikiParser {
|
||
|
|
type Token = VimwikiToken;
|
||
|
|
|
||
|
|
fn parse(&self, tokens: TokenStream<VimwikiToken>) -> DocumentNode {
|
||
|
|
let v = tokens.into_vec();
|
||
|
|
let mut state = ParseState::new(&v);
|
||
|
|
state.parse_document()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Cursor =====
|
||
|
|
|
||
|
|
struct ParseState<'a> {
|
||
|
|
toks: &'a [VimwikiToken],
|
||
|
|
pos: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> ParseState<'a> {
|
||
|
|
fn new(toks: &'a [VimwikiToken]) -> Self {
|
||
|
|
Self { toks, pos: 0 }
|
||
|
|
}
|
||
|
|
|
||
|
|
fn at_eof(&self) -> bool {
|
||
|
|
self.pos >= self.toks.len()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn peek(&self) -> Option<&'a VimwikiToken> {
|
||
|
|
self.toks.get(self.pos)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn peek_kind(&self) -> Option<&'a K> {
|
||
|
|
self.peek().map(|t| &t.kind)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn advance(&mut self) -> Option<&'a VimwikiToken> {
|
||
|
|
let t = self.toks.get(self.pos);
|
||
|
|
if t.is_some() {
|
||
|
|
self.pos += 1;
|
||
|
|
}
|
||
|
|
t
|
||
|
|
}
|
||
|
|
|
||
|
|
fn skip_blanks(&mut self) {
|
||
|
|
while matches!(self.peek_kind(), Some(K::Newline) | Some(K::BlankLine)) {
|
||
|
|
self.pos += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn eat_newline(&mut self) -> bool {
|
||
|
|
if matches!(self.peek_kind(), Some(K::Newline)) {
|
||
|
|
self.pos += 1;
|
||
|
|
true
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Document =====
|
||
|
|
|
||
|
|
fn parse_document(&mut self) -> DocumentNode {
|
||
|
|
let start_pos = self.toks.first().map(|t| t.span.start).unwrap_or_default();
|
||
|
|
let end_pos = self.toks.last().map(|t| t.span.end).unwrap_or(start_pos);
|
||
|
|
|
||
|
|
let mut metadata = PageMetadata::default();
|
||
|
|
let mut children = Vec::new();
|
||
|
|
|
||
|
|
loop {
|
||
|
|
self.skip_blanks();
|
||
|
|
if self.at_eof() {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if self.consume_placeholder(&mut metadata) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let before = self.pos;
|
||
|
|
children.push(self.parse_block());
|
||
|
|
// Resilience: guarantee forward progress.
|
||
|
|
if self.pos == before {
|
||
|
|
let span = self.toks.get(self.pos).map(|t| t.span).unwrap_or_default();
|
||
|
|
children.push(BlockNode::Error(ErrorNode {
|
||
|
|
span,
|
||
|
|
raw: format!("{:?}", self.toks.get(self.pos).map(|t| &t.kind)),
|
||
|
|
message: "parser made no progress".to_owned(),
|
||
|
|
}));
|
||
|
|
self.pos += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
DocumentNode {
|
||
|
|
span: Span::new(start_pos, end_pos),
|
||
|
|
children,
|
||
|
|
metadata,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn consume_placeholder(&mut self, metadata: &mut PageMetadata) -> bool {
|
||
|
|
let updated = match self.peek_kind() {
|
||
|
|
Some(K::PlaceholderTitle(v)) => {
|
||
|
|
metadata.title = v.clone();
|
||
|
|
true
|
||
|
|
}
|
||
|
|
Some(K::PlaceholderNohtml) => {
|
||
|
|
metadata.nohtml = true;
|
||
|
|
true
|
||
|
|
}
|
||
|
|
Some(K::PlaceholderTemplate(v)) => {
|
||
|
|
metadata.template = v.clone();
|
||
|
|
true
|
||
|
|
}
|
||
|
|
Some(K::PlaceholderDate(v)) => {
|
||
|
|
metadata.date = v.clone();
|
||
|
|
true
|
||
|
|
}
|
||
|
|
_ => false,
|
||
|
|
};
|
||
|
|
if updated {
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
}
|
||
|
|
updated
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Block dispatch =====
|
||
|
|
|
||
|
|
fn parse_block(&mut self) -> BlockNode {
|
||
|
|
let kind = self.peek_kind().expect("parse_block called at EOF");
|
||
|
|
match kind {
|
||
|
|
K::HeadingOpen { .. } => self.parse_heading(),
|
||
|
|
K::HorizontalRule => self.parse_horizontal_rule(),
|
||
|
|
K::ListMarker { .. } => self.parse_list(),
|
||
|
|
K::BlockquoteMarker | K::BlockquoteIndent => self.parse_blockquote(),
|
||
|
|
K::TableSep | K::TableHeaderRow => self.parse_table(),
|
||
|
|
K::PreformattedOpen { .. } => self.parse_preformatted(),
|
||
|
|
K::MathBlockOpen { .. } => self.parse_math_block(),
|
||
|
|
K::CommentLine(_) => self.parse_single_comment(),
|
||
|
|
K::CommentMultiOpen => self.parse_multi_comment(),
|
||
|
|
_ => {
|
||
|
|
if self.is_definition_start() {
|
||
|
|
self.parse_definition_list()
|
||
|
|
} else {
|
||
|
|
self.parse_paragraph()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Heading =====
|
||
|
|
|
||
|
|
fn parse_heading(&mut self) -> BlockNode {
|
||
|
|
let open = self.advance().unwrap();
|
||
|
|
let span_start = open.span.start;
|
||
|
|
let (level, centered) = match &open.kind {
|
||
|
|
K::HeadingOpen { level, centered } => (*level, *centered),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
let inline_start = self.pos;
|
||
|
|
let mut span_end = open.span.end;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::HeadingClose => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
let inline = parse_inline_seq(&self.toks[inline_start..self.pos]);
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
return BlockNode::Heading(HeadingNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
level,
|
||
|
|
centered,
|
||
|
|
children: inline,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// No closing `=` seen: emit a paragraph from what we collected.
|
||
|
|
let inline = parse_inline_seq(&self.toks[inline_start..self.pos]);
|
||
|
|
self.eat_newline();
|
||
|
|
BlockNode::Paragraph(ParagraphNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
children: inline,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Horizontal rule =====
|
||
|
|
|
||
|
|
fn parse_horizontal_rule(&mut self) -> BlockNode {
|
||
|
|
let t = self.advance().unwrap();
|
||
|
|
let span = t.span;
|
||
|
|
self.eat_newline();
|
||
|
|
BlockNode::HorizontalRule(HorizontalRuleNode { span })
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Comments =====
|
||
|
|
|
||
|
|
fn parse_single_comment(&mut self) -> BlockNode {
|
||
|
|
let t = self.advance().unwrap();
|
||
|
|
let content = match &t.kind {
|
||
|
|
K::CommentLine(s) => s.clone(),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
let span = t.span;
|
||
|
|
self.eat_newline();
|
||
|
|
BlockNode::Comment(CommentNode { span, content })
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_multi_comment(&mut self) -> BlockNode {
|
||
|
|
let open = self.advance().unwrap();
|
||
|
|
let span_start = open.span.start;
|
||
|
|
let mut span_end = open.span.end;
|
||
|
|
let mut content = String::new();
|
||
|
|
let mut first = true;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::CommentMultiClose => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
return BlockNode::Comment(CommentNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
K::CommentMultiLine(s) => {
|
||
|
|
if !first {
|
||
|
|
content.push('\n');
|
||
|
|
}
|
||
|
|
content.push_str(s);
|
||
|
|
first = false;
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
K::Newline => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Unterminated: keep what we have.
|
||
|
|
BlockNode::Comment(CommentNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Preformatted =====
|
||
|
|
|
||
|
|
fn parse_preformatted(&mut self) -> BlockNode {
|
||
|
|
let open = self.advance().unwrap();
|
||
|
|
let span_start = open.span.start;
|
||
|
|
let mut span_end = open.span.end;
|
||
|
|
let language = match &open.kind {
|
||
|
|
K::PreformattedOpen { language, .. } => language.clone(),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
let mut content = String::new();
|
||
|
|
let mut first = true;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::PreformattedClose => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
return BlockNode::Preformatted(PreformattedNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
language,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
K::PreformattedLine(s) => {
|
||
|
|
if !first {
|
||
|
|
content.push('\n');
|
||
|
|
}
|
||
|
|
content.push_str(s);
|
||
|
|
first = false;
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
K::Newline => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
BlockNode::Preformatted(PreformattedNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
language,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Math block =====
|
||
|
|
|
||
|
|
fn parse_math_block(&mut self) -> BlockNode {
|
||
|
|
let open = self.advance().unwrap();
|
||
|
|
let span_start = open.span.start;
|
||
|
|
let mut span_end = open.span.end;
|
||
|
|
let environment = match &open.kind {
|
||
|
|
K::MathBlockOpen { environment } => environment.clone(),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
let mut content = String::new();
|
||
|
|
let mut first = true;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::MathBlockClose => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
return BlockNode::MathBlock(MathBlockNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
environment,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
K::MathBlockLine(s) => {
|
||
|
|
if !first {
|
||
|
|
content.push('\n');
|
||
|
|
}
|
||
|
|
content.push_str(s);
|
||
|
|
first = false;
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
K::Newline => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
BlockNode::MathBlock(MathBlockNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
content,
|
||
|
|
environment,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== List =====
|
||
|
|
|
||
|
|
fn parse_list(&mut self) -> BlockNode {
|
||
|
|
let first = self.peek().expect("parse_list at EOF");
|
||
|
|
let span_start = first.span.start;
|
||
|
|
let (base_symbol, base_indent) = match &first.kind {
|
||
|
|
K::ListMarker { symbol, indent } => (*symbol, *indent),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
let mut items = Vec::new();
|
||
|
|
let mut span_end = span_start;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::ListMarker { indent, .. } if *indent == base_indent => {
|
||
|
|
let item = self.parse_list_item();
|
||
|
|
span_end = item.span.end;
|
||
|
|
items.push(item);
|
||
|
|
}
|
||
|
|
_ => break,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
BlockNode::List(ListNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
ordered: matches!(
|
||
|
|
base_symbol,
|
||
|
|
ListSymbol::Numeric
|
||
|
|
| ListSymbol::NumericParen
|
||
|
|
| ListSymbol::AlphaParen
|
||
|
|
| ListSymbol::AlphaUpperParen
|
||
|
|
| ListSymbol::RomanParen
|
||
|
|
| ListSymbol::RomanUpperParen
|
||
|
|
),
|
||
|
|
symbol: base_symbol,
|
||
|
|
items,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_list_item(&mut self) -> ListItemNode {
|
||
|
|
let marker = self.advance().expect("list item without marker");
|
||
|
|
let span_start = marker.span.start;
|
||
|
|
let mut span_end = marker.span.end;
|
||
|
|
let (symbol, level) = match &marker.kind {
|
||
|
|
K::ListMarker { symbol, indent } => (*symbol, *indent as usize),
|
||
|
|
_ => unreachable!(),
|
||
|
|
};
|
||
|
|
|
||
|
|
// Optional Checkbox
|
||
|
|
let checkbox = if let Some(K::Checkbox(state)) = self.peek_kind() {
|
||
|
|
let state = *state;
|
||
|
|
let t = self.advance().unwrap();
|
||
|
|
span_end = t.span.end;
|
||
|
|
Some(state)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
|
||
|
|
// Inline content until end of line.
|
||
|
|
let inline_start = self.pos;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let inline_end = self.pos;
|
||
|
|
let children = parse_inline_seq(&self.toks[inline_start..inline_end]);
|
||
|
|
self.eat_newline();
|
||
|
|
|
||
|
|
// Sublist: subsequent ListMarker tokens with deeper indent.
|
||
|
|
let sublist = if let Some(K::ListMarker { indent, .. }) = self.peek_kind() {
|
||
|
|
if (*indent as usize) > level {
|
||
|
|
let block = self.parse_list();
|
||
|
|
if let BlockNode::List(node) = block {
|
||
|
|
span_end = node.span.end;
|
||
|
|
Some(node)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
|
||
|
|
ListItemNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
symbol,
|
||
|
|
level,
|
||
|
|
checkbox,
|
||
|
|
children,
|
||
|
|
sublist,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Blockquote =====
|
||
|
|
|
||
|
|
fn parse_blockquote(&mut self) -> BlockNode {
|
||
|
|
let first = self.peek().unwrap();
|
||
|
|
let span_start = first.span.start;
|
||
|
|
let mut span_end = span_start;
|
||
|
|
let mut lines: Vec<Vec<InlineNode>> = Vec::new();
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::BlockquoteMarker | K::BlockquoteIndent => {
|
||
|
|
self.advance();
|
||
|
|
let inline_start = self.pos;
|
||
|
|
while let Some(t2) = self.peek() {
|
||
|
|
match &t2.kind {
|
||
|
|
K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
span_end = t2.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let inline_end = self.pos;
|
||
|
|
let inline = parse_inline_seq(&self.toks[inline_start..inline_end]);
|
||
|
|
if !inline.is_empty() {
|
||
|
|
lines.push(inline);
|
||
|
|
}
|
||
|
|
self.eat_newline();
|
||
|
|
}
|
||
|
|
_ => break,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let mut children: Vec<BlockNode> = Vec::with_capacity(lines.len());
|
||
|
|
for line in lines {
|
||
|
|
let line_start = line
|
||
|
|
.first()
|
||
|
|
.and_then(span_start_of_inline)
|
||
|
|
.unwrap_or(span_start);
|
||
|
|
let line_end = line.last().and_then(span_end_of_inline).unwrap_or(span_end);
|
||
|
|
children.push(BlockNode::Paragraph(ParagraphNode {
|
||
|
|
span: Span::new(line_start, line_end),
|
||
|
|
children: line,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
BlockNode::Blockquote(BlockquoteNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
children,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Table =====
|
||
|
|
|
||
|
|
fn parse_table(&mut self) -> BlockNode {
|
||
|
|
let first = self.peek().unwrap();
|
||
|
|
let span_start = first.span.start;
|
||
|
|
let mut span_end = span_start;
|
||
|
|
let mut rows: Vec<TableRowNode> = Vec::new();
|
||
|
|
let mut next_is_header = false;
|
||
|
|
let mut has_header = false;
|
||
|
|
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::TableHeaderRow => {
|
||
|
|
has_header = true;
|
||
|
|
if let Some(last) = rows.last_mut() {
|
||
|
|
last.is_header = true;
|
||
|
|
}
|
||
|
|
next_is_header = true;
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
self.eat_newline();
|
||
|
|
}
|
||
|
|
K::TableSep => {
|
||
|
|
let row = self.parse_table_row();
|
||
|
|
span_end = row.span.end;
|
||
|
|
rows.push(row);
|
||
|
|
if next_is_header {
|
||
|
|
// Header sep applies to the *previous* row, not this one.
|
||
|
|
next_is_header = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => break,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
BlockNode::Table(TableNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
rows,
|
||
|
|
has_header,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_table_row(&mut self) -> TableRowNode {
|
||
|
|
// Consume leading TableSep
|
||
|
|
let first_sep = self.advance().expect("table row without leading |");
|
||
|
|
let span_start = first_sep.span.start;
|
||
|
|
let mut span_end = first_sep.span.end;
|
||
|
|
let mut cells: Vec<TableCellNode> = Vec::new();
|
||
|
|
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::Newline | K::BlankLine => break,
|
||
|
|
K::TableSep => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
K::TableColSpan => {
|
||
|
|
let span = t.span;
|
||
|
|
span_end = span.end;
|
||
|
|
self.advance();
|
||
|
|
cells.push(TableCellNode {
|
||
|
|
span,
|
||
|
|
children: Vec::new(),
|
||
|
|
col_span: true,
|
||
|
|
row_span: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
K::TableRowSpan => {
|
||
|
|
let span = t.span;
|
||
|
|
span_end = span.end;
|
||
|
|
self.advance();
|
||
|
|
cells.push(TableCellNode {
|
||
|
|
span,
|
||
|
|
children: Vec::new(),
|
||
|
|
col_span: false,
|
||
|
|
row_span: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
_ => {
|
||
|
|
let cell_start_idx = self.pos;
|
||
|
|
let cell_start_pos = t.span.start;
|
||
|
|
let mut cell_end_pos = t.span.end;
|
||
|
|
while let Some(tt) = self.peek() {
|
||
|
|
match &tt.kind {
|
||
|
|
K::TableSep | K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
cell_end_pos = tt.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let inline = parse_inline_seq(&self.toks[cell_start_idx..self.pos]);
|
||
|
|
span_end = cell_end_pos;
|
||
|
|
cells.push(TableCellNode {
|
||
|
|
span: Span::new(cell_start_pos, cell_end_pos),
|
||
|
|
children: inline,
|
||
|
|
col_span: false,
|
||
|
|
row_span: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.eat_newline();
|
||
|
|
TableRowNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
cells,
|
||
|
|
is_header: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Definition list =====
|
||
|
|
|
||
|
|
fn is_definition_start(&self) -> bool {
|
||
|
|
// Walk forward on the current line. If we find a DefinitionTermMarker
|
||
|
|
// before a Newline/BlankLine, treat as a definition.
|
||
|
|
let mut i = self.pos;
|
||
|
|
while let Some(t) = self.toks.get(i) {
|
||
|
|
match &t.kind {
|
||
|
|
K::Newline | K::BlankLine => return false,
|
||
|
|
K::DefinitionTermMarker => return true,
|
||
|
|
_ => i += 1,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
false
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_definition_list(&mut self) -> BlockNode {
|
||
|
|
let first = self.peek().unwrap();
|
||
|
|
let span_start = first.span.start;
|
||
|
|
let mut span_end = span_start;
|
||
|
|
let mut items: Vec<DefinitionItemNode> = Vec::new();
|
||
|
|
while self.is_definition_start() {
|
||
|
|
let item = self.parse_definition_item();
|
||
|
|
span_end = item.span.end;
|
||
|
|
items.push(item);
|
||
|
|
}
|
||
|
|
BlockNode::DefinitionList(DefinitionListNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
items,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_definition_item(&mut self) -> DefinitionItemNode {
|
||
|
|
let item_start_idx = self.pos;
|
||
|
|
let item_span_start = self.toks[item_start_idx].span.start;
|
||
|
|
|
||
|
|
// Term: tokens until DefinitionTermMarker
|
||
|
|
let term_start = self.pos;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::DefinitionTermMarker | K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let term_end = self.pos;
|
||
|
|
let term_inline = if term_end > term_start {
|
||
|
|
let v = parse_inline_seq(&self.toks[term_start..term_end]);
|
||
|
|
if v.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some(v)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut span_end = self
|
||
|
|
.toks
|
||
|
|
.get(term_end.saturating_sub(1))
|
||
|
|
.map(|t| t.span.end)
|
||
|
|
.unwrap_or(item_span_start);
|
||
|
|
|
||
|
|
// DefinitionTermMarker
|
||
|
|
if matches!(self.peek_kind(), Some(K::DefinitionTermMarker)) {
|
||
|
|
span_end = self.peek().unwrap().span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Definition tokens until end of line
|
||
|
|
let def_start = self.pos;
|
||
|
|
while let Some(t) = self.peek() {
|
||
|
|
match &t.kind {
|
||
|
|
K::Newline | K::BlankLine => break,
|
||
|
|
_ => {
|
||
|
|
span_end = t.span.end;
|
||
|
|
self.advance();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let def_end = self.pos;
|
||
|
|
let mut definitions = Vec::new();
|
||
|
|
if def_end > def_start {
|
||
|
|
let v = parse_inline_seq(&self.toks[def_start..def_end]);
|
||
|
|
if !v.is_empty() {
|
||
|
|
definitions.push(v);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.eat_newline();
|
||
|
|
|
||
|
|
DefinitionItemNode {
|
||
|
|
span: Span::new(item_span_start, span_end),
|
||
|
|
term: term_inline,
|
||
|
|
definitions,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Paragraph =====
|
||
|
|
|
||
|
|
fn parse_paragraph(&mut self) -> BlockNode {
|
||
|
|
let span_start = self.peek().unwrap().span.start;
|
||
|
|
let inline_start = self.pos;
|
||
|
|
// `content_end` excludes a trailing newline that just terminates
|
||
|
|
// the paragraph (instead of joining two content lines), so it
|
||
|
|
// doesn't leak into `parse_inline_seq` as a stray soft-break " ".
|
||
|
|
let mut content_end = self.pos;
|
||
|
|
let mut span_end = span_start;
|
||
|
|
|
||
|
|
loop {
|
||
|
|
match self.peek_kind() {
|
||
|
|
None => break,
|
||
|
|
Some(K::Newline) => {
|
||
|
|
span_end = self.peek().unwrap().span.end;
|
||
|
|
self.advance();
|
||
|
|
match self.peek_kind() {
|
||
|
|
None => break,
|
||
|
|
Some(k) if starts_new_block(k) => break,
|
||
|
|
// Soft-break: the newline joins two content lines.
|
||
|
|
// Pull it into the inline range as a space.
|
||
|
|
_ => content_end = self.pos,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Some(k) if starts_new_block(k) => break,
|
||
|
|
Some(_) => {
|
||
|
|
span_end = self.peek().unwrap().span.end;
|
||
|
|
self.advance();
|
||
|
|
content_end = self.pos;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let children = parse_inline_seq(&self.toks[inline_start..content_end]);
|
||
|
|
BlockNode::Paragraph(ParagraphNode {
|
||
|
|
span: Span::new(span_start, span_end),
|
||
|
|
children,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn starts_new_block(kind: &K) -> bool {
|
||
|
|
matches!(
|
||
|
|
kind,
|
||
|
|
K::BlankLine
|
||
|
|
| K::HeadingOpen { .. }
|
||
|
|
| K::HorizontalRule
|
||
|
|
| K::ListMarker { .. }
|
||
|
|
| K::BlockquoteMarker
|
||
|
|
| K::BlockquoteIndent
|
||
|
|
| K::TableSep
|
||
|
|
| K::TableHeaderRow
|
||
|
|
| K::PreformattedOpen { .. }
|
||
|
|
| K::MathBlockOpen { .. }
|
||
|
|
| K::CommentLine(_)
|
||
|
|
| K::CommentMultiOpen
|
||
|
|
| K::PlaceholderTitle(_)
|
||
|
|
| K::PlaceholderNohtml
|
||
|
|
| K::PlaceholderTemplate(_)
|
||
|
|
| K::PlaceholderDate(_)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Inline pairing =====
|
||
|
|
|
||
|
|
fn parse_inline_seq(toks: &[VimwikiToken]) -> Vec<InlineNode> {
|
||
|
|
let mut out = Vec::new();
|
||
|
|
let mut i = 0;
|
||
|
|
while i < toks.len() {
|
||
|
|
let token = &toks[i];
|
||
|
|
match &token.kind {
|
||
|
|
K::Text(s) => {
|
||
|
|
out.push(InlineNode::Text(TextNode {
|
||
|
|
span: token.span,
|
||
|
|
content: s.clone(),
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::Code(s) => {
|
||
|
|
out.push(InlineNode::Code(CodeNode {
|
||
|
|
span: token.span,
|
||
|
|
content: s.clone(),
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::MathInline(s) => {
|
||
|
|
out.push(InlineNode::MathInline(MathInlineNode {
|
||
|
|
span: token.span,
|
||
|
|
content: s.clone(),
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::Keyword(k) => {
|
||
|
|
out.push(InlineNode::Keyword(KeywordNode {
|
||
|
|
span: token.span,
|
||
|
|
keyword: *k,
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::RawUrl(u) => {
|
||
|
|
out.push(InlineNode::RawUrl(RawUrlNode {
|
||
|
|
span: token.span,
|
||
|
|
url: u.clone(),
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::Newline => {
|
||
|
|
// Soft break inside a multi-line block: render as space.
|
||
|
|
out.push(InlineNode::Text(TextNode {
|
||
|
|
span: token.span,
|
||
|
|
content: " ".into(),
|
||
|
|
}));
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
K::BoldDelim => {
|
||
|
|
i = pair_or_text(toks, i, "*", &mut out, K::BoldDelim, |span, kids| {
|
||
|
|
InlineNode::Bold(BoldNode {
|
||
|
|
span,
|
||
|
|
children: kids,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
K::ItalicDelim => {
|
||
|
|
i = pair_or_text(toks, i, "_", &mut out, K::ItalicDelim, |span, kids| {
|
||
|
|
InlineNode::Italic(ItalicNode {
|
||
|
|
span,
|
||
|
|
children: kids,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
K::StrikethroughDelim => {
|
||
|
|
i = pair_or_text(
|
||
|
|
toks,
|
||
|
|
i,
|
||
|
|
"~~",
|
||
|
|
&mut out,
|
||
|
|
K::StrikethroughDelim,
|
||
|
|
|span, kids| {
|
||
|
|
InlineNode::Strikethrough(StrikethroughNode {
|
||
|
|
span,
|
||
|
|
children: kids,
|
||
|
|
})
|
||
|
|
},
|
||
|
|
)
|
||
|
|
}
|
||
|
|
K::SuperscriptDelim => {
|
||
|
|
i = pair_or_text(toks, i, "^", &mut out, K::SuperscriptDelim, |span, kids| {
|
||
|
|
InlineNode::Superscript(SuperscriptNode {
|
||
|
|
span,
|
||
|
|
children: kids,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
K::SubscriptDelim => {
|
||
|
|
i = pair_or_text(toks, i, ",,", &mut out, K::SubscriptDelim, |span, kids| {
|
||
|
|
InlineNode::Subscript(SubscriptNode {
|
||
|
|
span,
|
||
|
|
children: kids,
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
K::WikiLinkOpen => {
|
||
|
|
let (node, next) = parse_wikilink(toks, i);
|
||
|
|
out.push(node);
|
||
|
|
i = next;
|
||
|
|
}
|
||
|
|
K::TransclusionOpen => {
|
||
|
|
let (node, next) = parse_transclusion(toks, i);
|
||
|
|
out.push(node);
|
||
|
|
i = next;
|
||
|
|
}
|
||
|
|
// Tokens that shouldn't appear in inline context (e.g. dangling
|
||
|
|
// close delimiters, separators outside their owners): emit as
|
||
|
|
// literal text so nothing is silently dropped.
|
||
|
|
K::WikiLinkClose => push_literal(token, "]]", &mut out, &mut i),
|
||
|
|
K::WikiLinkSep => push_literal(token, "|", &mut out, &mut i),
|
||
|
|
K::TransclusionClose => push_literal(token, "}}", &mut out, &mut i),
|
||
|
|
K::TransclusionSep => push_literal(token, "|", &mut out, &mut i),
|
||
|
|
K::HeadingClose => push_literal(token, "=", &mut out, &mut i),
|
||
|
|
// Other tokens (block markers, etc.) should not appear here; skip.
|
||
|
|
_ => {
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
fn push_literal(token: &VimwikiToken, lit: &str, out: &mut Vec<InlineNode>, i: &mut usize) {
|
||
|
|
out.push(InlineNode::Text(TextNode {
|
||
|
|
span: token.span,
|
||
|
|
content: lit.into(),
|
||
|
|
}));
|
||
|
|
*i += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
fn pair_or_text<F>(
|
||
|
|
toks: &[VimwikiToken],
|
||
|
|
open_idx: usize,
|
||
|
|
literal: &str,
|
||
|
|
out: &mut Vec<InlineNode>,
|
||
|
|
target: K,
|
||
|
|
build: F,
|
||
|
|
) -> usize
|
||
|
|
where
|
||
|
|
F: FnOnce(Span, Vec<InlineNode>) -> InlineNode,
|
||
|
|
{
|
||
|
|
if let Some(close_idx) = find_close(toks, open_idx, &target) {
|
||
|
|
let inner = parse_inline_seq(&toks[open_idx + 1..close_idx]);
|
||
|
|
let span = Span::new(toks[open_idx].span.start, toks[close_idx].span.end);
|
||
|
|
out.push(build(span, inner));
|
||
|
|
close_idx + 1
|
||
|
|
} else {
|
||
|
|
out.push(InlineNode::Text(TextNode {
|
||
|
|
span: toks[open_idx].span,
|
||
|
|
content: literal.into(),
|
||
|
|
}));
|
||
|
|
open_idx + 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn find_close(toks: &[VimwikiToken], start: usize, target: &K) -> Option<usize> {
|
||
|
|
for (offset, t) in toks.iter().enumerate().skip(start + 1) {
|
||
|
|
if &t.kind == target {
|
||
|
|
return Some(offset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Wikilink + transclusion =====
|
||
|
|
|
||
|
|
fn parse_wikilink(toks: &[VimwikiToken], open_idx: usize) -> (InlineNode, usize) {
|
||
|
|
let span_start = toks[open_idx].span.start;
|
||
|
|
let mut i = open_idx + 1;
|
||
|
|
let target_start = i;
|
||
|
|
let mut target_end = i;
|
||
|
|
let mut sep_idx: Option<usize> = None;
|
||
|
|
let mut close_idx: Option<usize> = None;
|
||
|
|
while i < toks.len() {
|
||
|
|
match &toks[i].kind {
|
||
|
|
K::WikiLinkClose => {
|
||
|
|
close_idx = Some(i);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
K::WikiLinkSep if sep_idx.is_none() => {
|
||
|
|
sep_idx = Some(i);
|
||
|
|
target_end = i;
|
||
|
|
}
|
||
|
|
_ => {}
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
let close_idx = match close_idx {
|
||
|
|
Some(c) => c,
|
||
|
|
None => {
|
||
|
|
// Unterminated link: emit literal `[[`.
|
||
|
|
return (
|
||
|
|
InlineNode::Text(TextNode {
|
||
|
|
span: toks[open_idx].span,
|
||
|
|
content: "[[".into(),
|
||
|
|
}),
|
||
|
|
open_idx + 1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if sep_idx.is_none() {
|
||
|
|
target_end = close_idx;
|
||
|
|
}
|
||
|
|
|
||
|
|
let target_text = collect_text(&toks[target_start..target_end]);
|
||
|
|
let span = Span::new(span_start, toks[close_idx].span.end);
|
||
|
|
|
||
|
|
let description: Option<Vec<InlineNode>> = sep_idx.map(|sep| {
|
||
|
|
let inner = parse_inline_seq(&toks[sep + 1..close_idx]);
|
||
|
|
if inner.is_empty() {
|
||
|
|
vec![]
|
||
|
|
} else {
|
||
|
|
inner
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if is_url(&target_text) {
|
||
|
|
(
|
||
|
|
InlineNode::ExternalLink(ExternalLinkNode {
|
||
|
|
span,
|
||
|
|
url: target_text,
|
||
|
|
description,
|
||
|
|
}),
|
||
|
|
close_idx + 1,
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
let target = parse_link_target(&target_text);
|
||
|
|
(
|
||
|
|
InlineNode::WikiLink(WikiLinkNode {
|
||
|
|
span,
|
||
|
|
target,
|
||
|
|
description,
|
||
|
|
}),
|
||
|
|
close_idx + 1,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_transclusion(toks: &[VimwikiToken], open_idx: usize) -> (InlineNode, usize) {
|
||
|
|
let span_start = toks[open_idx].span.start;
|
||
|
|
let mut i = open_idx + 1;
|
||
|
|
let url_start = i;
|
||
|
|
let mut url_end = i;
|
||
|
|
let mut seps: Vec<usize> = Vec::new();
|
||
|
|
let mut close_idx: Option<usize> = None;
|
||
|
|
while i < toks.len() {
|
||
|
|
match &toks[i].kind {
|
||
|
|
K::TransclusionClose => {
|
||
|
|
close_idx = Some(i);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
K::TransclusionSep => {
|
||
|
|
if seps.is_empty() {
|
||
|
|
url_end = i;
|
||
|
|
}
|
||
|
|
seps.push(i);
|
||
|
|
}
|
||
|
|
_ => {}
|
||
|
|
}
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
let close_idx = match close_idx {
|
||
|
|
Some(c) => c,
|
||
|
|
None => {
|
||
|
|
return (
|
||
|
|
InlineNode::Text(TextNode {
|
||
|
|
span: toks[open_idx].span,
|
||
|
|
content: "{{".into(),
|
||
|
|
}),
|
||
|
|
open_idx + 1,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if seps.is_empty() {
|
||
|
|
url_end = close_idx;
|
||
|
|
}
|
||
|
|
let url = collect_text(&toks[url_start..url_end]);
|
||
|
|
let span = Span::new(span_start, toks[close_idx].span.end);
|
||
|
|
|
||
|
|
// After URL: optional alt, then key=val attrs.
|
||
|
|
let mut alt = None;
|
||
|
|
let mut attrs = std::collections::HashMap::new();
|
||
|
|
if !seps.is_empty() {
|
||
|
|
let alt_start = seps[0] + 1;
|
||
|
|
let alt_end = if seps.len() >= 2 { seps[1] } else { close_idx };
|
||
|
|
let alt_text = collect_text(&toks[alt_start..alt_end]);
|
||
|
|
if !alt_text.is_empty() {
|
||
|
|
alt = Some(alt_text);
|
||
|
|
}
|
||
|
|
for window in seps.windows(2) {
|
||
|
|
let s = window[0];
|
||
|
|
let e = window[1];
|
||
|
|
// attr segment is at [s+1..e]; format key=val
|
||
|
|
let segment = collect_text(&toks[s + 1..e]);
|
||
|
|
if let Some(eq) = segment.find('=') {
|
||
|
|
let k = segment[..eq].trim().to_owned();
|
||
|
|
let v = segment[eq + 1..].trim().to_owned();
|
||
|
|
if !k.is_empty() {
|
||
|
|
attrs.insert(k, v);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Last attribute segment between final sep and close.
|
||
|
|
if let Some(&last) = seps.last() {
|
||
|
|
if last + 1 < close_idx && seps.len() >= 2 {
|
||
|
|
let segment = collect_text(&toks[last + 1..close_idx]);
|
||
|
|
if let Some(eq) = segment.find('=') {
|
||
|
|
let k = segment[..eq].trim().to_owned();
|
||
|
|
let v = segment[eq + 1..].trim().to_owned();
|
||
|
|
if !k.is_empty() {
|
||
|
|
attrs.insert(k, v);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
(
|
||
|
|
InlineNode::Transclusion(TransclusionNode {
|
||
|
|
span,
|
||
|
|
url,
|
||
|
|
alt,
|
||
|
|
attrs,
|
||
|
|
}),
|
||
|
|
close_idx + 1,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn collect_text(toks: &[VimwikiToken]) -> String {
|
||
|
|
let mut out = String::new();
|
||
|
|
for t in toks {
|
||
|
|
if let K::Text(s) = &t.kind {
|
||
|
|
out.push_str(s);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_url(s: &str) -> bool {
|
||
|
|
s.starts_with("http://")
|
||
|
|
|| s.starts_with("https://")
|
||
|
|
|| s.starts_with("ftp://")
|
||
|
|
|| s.starts_with("mailto:")
|
||
|
|
|| s.starts_with("file://")
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== LinkTarget classification =====
|
||
|
|
|
||
|
|
fn parse_link_target(text: &str) -> LinkTarget {
|
||
|
|
let mut target = LinkTarget {
|
||
|
|
kind: LinkKind::Wiki,
|
||
|
|
path: None,
|
||
|
|
wiki_index: None,
|
||
|
|
wiki_name: None,
|
||
|
|
anchor: None,
|
||
|
|
is_absolute: false,
|
||
|
|
is_directory: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Split off anchor first.
|
||
|
|
let (path_part, anchor) = match text.split_once('#') {
|
||
|
|
Some((p, a)) => (p, Some(a.to_owned())),
|
||
|
|
None => (text, None),
|
||
|
|
};
|
||
|
|
target.anchor = anchor;
|
||
|
|
|
||
|
|
if path_part.is_empty() {
|
||
|
|
target.kind = LinkKind::AnchorOnly;
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
|
||
|
|
// `//path` — filesystem-absolute.
|
||
|
|
if let Some(rest) = path_part.strip_prefix("//") {
|
||
|
|
target.kind = LinkKind::Local;
|
||
|
|
target.is_absolute = true;
|
||
|
|
let (path, is_dir) = strip_directory(rest);
|
||
|
|
target.is_directory = is_dir;
|
||
|
|
target.path = Some(path);
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
|
||
|
|
// `/Page` — root-relative wiki link.
|
||
|
|
if let Some(rest) = path_part.strip_prefix('/') {
|
||
|
|
target.kind = LinkKind::Wiki;
|
||
|
|
target.is_absolute = true;
|
||
|
|
let (path, is_dir) = strip_directory(rest);
|
||
|
|
target.is_directory = is_dir;
|
||
|
|
target.path = Some(path);
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scheme prefixes.
|
||
|
|
if let Some(rest) = path_part.strip_prefix("file:") {
|
||
|
|
target.kind = LinkKind::File;
|
||
|
|
target.path = Some(rest.to_owned());
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
if let Some(rest) = path_part.strip_prefix("local:") {
|
||
|
|
target.kind = LinkKind::Local;
|
||
|
|
target.path = Some(rest.to_owned());
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
if let Some(rest) = path_part.strip_prefix("diary:") {
|
||
|
|
target.kind = LinkKind::Diary;
|
||
|
|
target.path = Some(rest.to_owned());
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
if let Some(rest) = path_part.strip_prefix("wn.") {
|
||
|
|
if let Some((name, page)) = rest.split_once(':') {
|
||
|
|
target.kind = LinkKind::Interwiki;
|
||
|
|
target.wiki_name = Some(name.to_owned());
|
||
|
|
target.path = Some(page.to_owned());
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if let Some((scheme, rest)) = path_part.split_once(':') {
|
||
|
|
if let Some(num) = scheme.strip_prefix("wiki") {
|
||
|
|
if let Ok(idx) = num.parse::<usize>() {
|
||
|
|
target.kind = LinkKind::Interwiki;
|
||
|
|
target.wiki_index = Some(idx);
|
||
|
|
target.path = Some(rest.to_owned());
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default: plain wiki link, possibly directory.
|
||
|
|
let (path, is_dir) = strip_directory(path_part);
|
||
|
|
target.kind = LinkKind::Wiki;
|
||
|
|
target.is_directory = is_dir;
|
||
|
|
target.path = Some(path);
|
||
|
|
target
|
||
|
|
}
|
||
|
|
|
||
|
|
fn strip_directory(s: &str) -> (String, bool) {
|
||
|
|
if let Some(stripped) = s.strip_suffix('/') {
|
||
|
|
(stripped.to_owned(), true)
|
||
|
|
} else {
|
||
|
|
(s.to_owned(), false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Span helpers =====
|
||
|
|
|
||
|
|
fn span_start_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
||
|
|
Some(span_of_inline(node).start)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn span_end_of_inline(node: &InlineNode) -> Option<crate::ast::Position> {
|
||
|
|
Some(span_of_inline(node).end)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn span_of_inline(node: &InlineNode) -> Span {
|
||
|
|
match node {
|
||
|
|
InlineNode::Text(n) => n.span,
|
||
|
|
InlineNode::Bold(n) => n.span,
|
||
|
|
InlineNode::Italic(n) => n.span,
|
||
|
|
InlineNode::BoldItalic(n) => n.span,
|
||
|
|
InlineNode::Strikethrough(n) => n.span,
|
||
|
|
InlineNode::Code(n) => n.span,
|
||
|
|
InlineNode::Superscript(n) => n.span,
|
||
|
|
InlineNode::Subscript(n) => n.span,
|
||
|
|
InlineNode::MathInline(n) => n.span,
|
||
|
|
InlineNode::Keyword(n) => n.span,
|
||
|
|
InlineNode::Color(n) => n.span,
|
||
|
|
InlineNode::WikiLink(n) => n.span,
|
||
|
|
InlineNode::ExternalLink(n) => n.span,
|
||
|
|
InlineNode::Transclusion(n) => n.span,
|
||
|
|
InlineNode::RawUrl(n) => n.span,
|
||
|
|
}
|
||
|
|
}
|