Fix broken references when parsing markdown streams
This commit is contained in:
parent
57b553de2f
commit
569ef13ac9
4 changed files with 103 additions and 15 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -4459,9 +4459,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.11.3"
|
version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625"
|
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"getopts",
|
"getopts",
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ num-traits = "0.2"
|
||||||
ouroboros = "0.18"
|
ouroboros = "0.18"
|
||||||
palette = "0.7"
|
palette = "0.7"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
pulldown-cmark = "0.11"
|
pulldown-cmark = "0.12"
|
||||||
qrcode = { version = "0.13", default-features = false }
|
qrcode = { version = "0.13", default-features = false }
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
resvg = "0.42"
|
resvg = "0.42"
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ impl Markdown {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Preview(_) => Subscription::none(),
|
Mode::Preview(_) => Subscription::none(),
|
||||||
Mode::Stream { .. } => {
|
Mode::Stream { .. } => {
|
||||||
time::every(milliseconds(20)).map(|_| Message::NextToken)
|
time::every(milliseconds(10)).map(|_| Message::NextToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,9 @@ use crate::{column, container, rich_text, row, scrollable, span, text};
|
||||||
|
|
||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use core::text::Highlight;
|
pub use core::text::Highlight;
|
||||||
|
|
@ -69,9 +71,16 @@ pub use url::Url;
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Content {
|
pub struct Content {
|
||||||
items: Vec<Item>,
|
items: Vec<Item>,
|
||||||
|
incomplete: HashMap<usize, Section>,
|
||||||
state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Section {
|
||||||
|
content: String,
|
||||||
|
broken_links: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Content {
|
impl Content {
|
||||||
/// Creates a new empty [`Content`].
|
/// Creates a new empty [`Content`].
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|
@ -80,10 +89,9 @@ impl Content {
|
||||||
|
|
||||||
/// Creates some new [`Content`] by parsing the given Markdown.
|
/// Creates some new [`Content`] by parsing the given Markdown.
|
||||||
pub fn parse(markdown: &str) -> Self {
|
pub fn parse(markdown: &str) -> Self {
|
||||||
let mut state = State::default();
|
let mut content = Self::new();
|
||||||
let items = parse_with(&mut state, markdown).collect();
|
content.push_str(markdown);
|
||||||
|
content
|
||||||
Self { items, state }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes more Markdown into the [`Content`]; parsing incrementally!
|
/// Pushes more Markdown into the [`Content`]; parsing incrementally!
|
||||||
|
|
@ -103,8 +111,52 @@ impl Content {
|
||||||
let _ = self.items.pop();
|
let _ = self.items.pop();
|
||||||
|
|
||||||
// Re-parse last item and new text
|
// Re-parse last item and new text
|
||||||
let new_items = parse_with(&mut self.state, &leftover);
|
for (item, source, broken_links) in
|
||||||
self.items.extend(new_items);
|
parse_with(&mut self.state, &leftover)
|
||||||
|
{
|
||||||
|
if !broken_links.is_empty() {
|
||||||
|
let _ = self.incomplete.insert(
|
||||||
|
self.items.len(),
|
||||||
|
Section {
|
||||||
|
content: source.to_owned(),
|
||||||
|
broken_links,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-parse incomplete sections if new references are available
|
||||||
|
if !self.incomplete.is_empty() {
|
||||||
|
let mut state = State {
|
||||||
|
leftover: String::new(),
|
||||||
|
references: self.state.references.clone(),
|
||||||
|
highlighter: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.incomplete.retain(|index, section| {
|
||||||
|
if self.items.len() <= *index {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let broken_links_before = section.broken_links.len();
|
||||||
|
|
||||||
|
section
|
||||||
|
.broken_links
|
||||||
|
.retain(|link| !self.state.references.contains_key(link));
|
||||||
|
|
||||||
|
if broken_links_before != section.broken_links.len() {
|
||||||
|
if let Some((item, _source, _broken_links)) =
|
||||||
|
parse_with(&mut state, §ion.content).next()
|
||||||
|
{
|
||||||
|
self.items[*index] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!section.broken_links.is_empty()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Markdown items, ready to be rendered.
|
/// Returns the Markdown items, ready to be rendered.
|
||||||
|
|
@ -285,11 +337,13 @@ impl Span {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
||||||
parse_with(State::default(), markdown)
|
parse_with(State::default(), markdown)
|
||||||
|
.map(|(item, _source, _broken_links)| item)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct State {
|
struct State {
|
||||||
leftover: String,
|
leftover: String,
|
||||||
|
references: HashMap<String, String>,
|
||||||
#[cfg(feature = "highlighter")]
|
#[cfg(feature = "highlighter")]
|
||||||
highlighter: Option<Highlighter>,
|
highlighter: Option<Highlighter>,
|
||||||
}
|
}
|
||||||
|
|
@ -379,12 +433,14 @@ impl Highlighter {
|
||||||
fn parse_with<'a>(
|
fn parse_with<'a>(
|
||||||
mut state: impl BorrowMut<State> + 'a,
|
mut state: impl BorrowMut<State> + 'a,
|
||||||
markdown: &'a str,
|
markdown: &'a str,
|
||||||
) -> impl Iterator<Item = Item> + 'a {
|
) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
|
||||||
struct List {
|
struct List {
|
||||||
start: Option<u64>,
|
start: Option<u64>,
|
||||||
items: Vec<Vec<Item>>,
|
items: Vec<Vec<Item>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let broken_links = Rc::new(RefCell::new(HashSet::new()));
|
||||||
|
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
let mut code = Vec::new();
|
let mut code = Vec::new();
|
||||||
let mut strong = false;
|
let mut strong = false;
|
||||||
|
|
@ -398,14 +454,40 @@ fn parse_with<'a>(
|
||||||
#[cfg(feature = "highlighter")]
|
#[cfg(feature = "highlighter")]
|
||||||
let mut highlighter = None;
|
let mut highlighter = None;
|
||||||
|
|
||||||
let parser = pulldown_cmark::Parser::new_ext(
|
let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
|
||||||
markdown,
|
markdown,
|
||||||
pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
|
pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
|
||||||
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
|
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
|
||||||
| pulldown_cmark::Options::ENABLE_TABLES
|
| pulldown_cmark::Options::ENABLE_TABLES
|
||||||
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
|
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
|
||||||
)
|
{
|
||||||
.into_offset_iter();
|
let references = state.borrow().references.clone();
|
||||||
|
let broken_links = broken_links.clone();
|
||||||
|
|
||||||
|
Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
|
||||||
|
if let Some(reference) =
|
||||||
|
references.get(broken_link.reference.as_ref())
|
||||||
|
{
|
||||||
|
Some((
|
||||||
|
pulldown_cmark::CowStr::from(reference.to_owned()),
|
||||||
|
broken_link.reference.into_static(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let _ = RefCell::borrow_mut(&broken_links)
|
||||||
|
.insert(broken_link.reference.to_string());
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let references = &mut state.borrow_mut().references;
|
||||||
|
|
||||||
|
for reference in parser.reference_definitions().iter() {
|
||||||
|
let _ = references
|
||||||
|
.insert(reference.0.to_owned(), reference.1.dest.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let produce = move |state: &mut State,
|
let produce = move |state: &mut State,
|
||||||
lists: &mut Vec<List>,
|
lists: &mut Vec<List>,
|
||||||
|
|
@ -414,7 +496,11 @@ fn parse_with<'a>(
|
||||||
if lists.is_empty() {
|
if lists.is_empty() {
|
||||||
state.leftover = markdown[source.start..].to_owned();
|
state.leftover = markdown[source.start..].to_owned();
|
||||||
|
|
||||||
Some(item)
|
Some((
|
||||||
|
item,
|
||||||
|
&markdown[source.start..source.end],
|
||||||
|
broken_links.take(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
lists
|
lists
|
||||||
.last_mut()
|
.last_mut()
|
||||||
|
|
@ -428,6 +514,8 @@ fn parse_with<'a>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let parser = parser.into_offset_iter();
|
||||||
|
|
||||||
// We want to keep the `spans` capacity
|
// We want to keep the `spans` capacity
|
||||||
#[allow(clippy::drain_collect)]
|
#[allow(clippy::drain_collect)]
|
||||||
parser.filter_map(move |(event, source)| match event {
|
parser.filter_map(move |(event, source)| match event {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue