Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173//! Inline markdown events, turned into styled spans.//!//! One run of inline text can sit inside several nested styles at once,//! e.g. bold inside italic, so this walks the events keeping a stack of//! active styles and patches each new one on top with [`Style::patch`].//! A hard break starts a new [`Line`] inside the same [`Text`]; the//! caller wraps the whole thing with `crate::wrap::wrap_spans`, which//! wraps each line on its own.//!//! [`text`] renders the content of a block that is cleanly delimited by//! its own end tag, a paragraph or a heading, and consumes that end tag//! along with the content. [`run`] renders a tight list item's bare//! inline text instead, which has no wrapping tag of its own: it stops//! at whichever block-level event follows it, without consuming that//! event, so the caller can dispatch it as the item's next child.
use pulldown_cmark::{Event, LinkType, Tag, TagEnd};use ratatui::style::Style;use ratatui::text::{Line, Span, Text};
use super::{Events, style};
/// Turns the inline events of a block delimited by `end` into styled/// text, consuming the block's content and its `end` tag. The caller/// gets a cursor sitting on whatever follows the block.////// Every span starts from `base`, with each active style patched on top/// of it. A plain paragraph's `base` is [`Style::default`]; a heading's/// is its own [`style::heading`], and a block quote's is/// [`style::quote`], so that emphasis, a code span, or a link inside one/// of those still carries it.pub(super) fn text(events: &mut Events, end: TagEnd, base: Style) -> Text<'static> { let text = styled(events, base, |event| *event != Event::End(end)); // `styled` stops on the end tag without taking it, and the only other // way out is the end of the stream, so this reads the end tag or // nothing at all. events.next(); text}
/// Turns a tight list item's bare inline events into styled text. Its/// content has no wrapping block tag, so this stops as soon as the next/// event is not one of the inline events it knows how to render, leaving/// that event for the caller to read.pub(super) fn run(events: &mut Events, base: Style) -> Text<'static> { styled(events, base, is_inline_event)}
/// True for an event `run` renders itself: plain and coded text, a soft/// or hard break, raw HTML, the start of emphasis, strong, strikethrough,/// a link, or an image, and the end of one of those spans. Anything else,/// a block-level start or an unrelated end tag, is the boundary `run`/// stops at.fn is_inline_event(event: &Event<'static>) -> bool { matches!( event, Event::Text(_) | Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) | Event::SoftBreak | Event::HardBreak | Event::Start( Tag::Emphasis | Tag::Strong | Tag::Strikethrough | Tag::Link { .. } | Tag::Image { .. } ) | Event::End(TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough) )}
/// Walks inline events, starting from `base`, for as long as `keep_going`/// says the next event belongs to this run.fn styled( events: &mut Events, base: Style, keep_going: impl Fn(&Event<'static>) -> bool,) -> Text<'static> { let mut lines = Vec::new(); let mut spans: Vec<Span<'static>> = Vec::new(); let mut active = base; let mut stack: Vec<Style> = Vec::new();
while events.peek().is_some_and(&keep_going) { let event = events.next().expect("just peeked"); if let Event::Start(Tag::Emphasis) = &event { stack.push(active); active = active.patch(style::emphasis()); } if let Event::Start(Tag::Strong) = &event { stack.push(active); active = active.patch(style::strong()); } if let Event::Start(Tag::Strikethrough) = &event { stack.push(active); active = active.patch(style::strikethrough()); } if let Event::End(TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough) = &event { active = stack.pop().unwrap_or(base); } if let Event::Start(Tag::Link { link_type, .. }) = &event { let link_type = *link_type; let text = collect_text(events, TagEnd::Link); // A wikilink is an entity reference and renders bold, where a // regular link stays underlined to read as a URL. let link_style = match link_type { LinkType::WikiLink { .. } => style::wiki(), _ => style::link(), }; spans.push(Span::styled( display_text(link_type, text), active.patch(link_style), )); } if let Event::Start(Tag::Image { .. }) = &event { let text = collect_text(events, TagEnd::Image); spans.push(Span::styled(text, active)); } match event { Event::Code(code) => { spans.push(Span::styled( code.into_string(), active.patch(style::code()), )); } Event::Text(text) => spans.push(Span::styled(text.into_string(), active)), Event::Html(html) | Event::InlineHtml(html) => { spans.push(Span::styled(html.into_string(), active)); } Event::SoftBreak => spans.push(Span::styled(" ", active)), Event::HardBreak => lines.push(Line::from(std::mem::take(&mut spans))), _ => {} } } lines.push(Line::from(spans)); Text::from(lines)}
/// Consumes an inline element's content up to its own `end` tag, keeping/// only its text. A link's destination and an image's source never/// render; both share this same collection.fn collect_text(events: &mut Events, end: TagEnd) -> String { let mut text = String::new(); while let Some(event) = events.next() { if event == Event::End(end) { break; } if let Event::Text(t) | Event::Code(t) = event { text.push_str(&t); } } text}
/// A wikilink shows only the segment after its last `/`; every other/// link shows its text unchanged.fn display_text(link_type: LinkType, text: String) -> String { if let LinkType::WikiLink { .. } = link_type { text.rsplit('/') .next() .expect("rsplit always yields at least one segment") .to_string() } else { text }}
#[cfg(test)]#[path = "inline_tests.rs"]mod tests;