Skip to content

Commit 569ef13

Browse files
committed
Fix broken references when parsing markdown streams
1 parent 57b553d commit 569ef13

File tree

4 files changed

+103
-15
lines changed

4 files changed

+103
-15
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ num-traits = "0.2"
166166
ouroboros = "0.18"
167167
palette = "0.7"
168168
png = "0.17"
169-
pulldown-cmark = "0.11"
169+
pulldown-cmark = "0.12"
170170
qrcode = { version = "0.13", default-features = false }
171171
raw-window-handle = "0.6"
172172
resvg = "0.42"

examples/markdown/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ impl Markdown {
162162
match self.mode {
163163
Mode::Preview(_) => Subscription::none(),
164164
Mode::Stream { .. } => {
165-
time::every(milliseconds(20)).map(|_| Message::NextToken)
165+
time::every(milliseconds(10)).map(|_| Message::NextToken)
166166
}
167167
}
168168
}

widget/src/markdown.rs

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ use crate::{column, container, rich_text, row, scrollable, span, text};
5858

5959
use std::borrow::BorrowMut;
6060
use std::cell::{Cell, RefCell};
61+
use std::collections::{HashMap, HashSet};
6162
use std::ops::Range;
63+
use std::rc::Rc;
6264
use std::sync::Arc;
6365

6466
pub use core::text::Highlight;
@@ -69,9 +71,16 @@ pub use url::Url;
6971
#[derive(Debug, Default)]
7072
pub struct Content {
7173
items: Vec<Item>,
74+
incomplete: HashMap<usize, Section>,
7275
state: State,
7376
}
7477

78+
#[derive(Debug)]
79+
struct Section {
80+
content: String,
81+
broken_links: HashSet<String>,
82+
}
83+
7584
impl Content {
7685
/// Creates a new empty [`Content`].
7786
pub fn new() -> Self {
@@ -80,10 +89,9 @@ impl Content {
8089

8190
/// Creates some new [`Content`] by parsing the given Markdown.
8291
pub fn parse(markdown: &str) -> Self {
83-
let mut state = State::default();
84-
let items = parse_with(&mut state, markdown).collect();
85-
86-
Self { items, state }
92+
let mut content = Self::new();
93+
content.push_str(markdown);
94+
content
8795
}
8896

8997
/// Pushes more Markdown into the [`Content`]; parsing incrementally!
@@ -103,8 +111,52 @@ impl Content {
103111
let _ = self.items.pop();
104112

105113
// Re-parse last item and new text
106-
let new_items = parse_with(&mut self.state, &leftover);
107-
self.items.extend(new_items);
114+
for (item, source, broken_links) in
115+
parse_with(&mut self.state, &leftover)
116+
{
117+
if !broken_links.is_empty() {
118+
let _ = self.incomplete.insert(
119+
self.items.len(),
120+
Section {
121+
content: source.to_owned(),
122+
broken_links,
123+
},
124+
);
125+
}
126+
127+
self.items.push(item);
128+
}
129+
130+
// Re-parse incomplete sections if new references are available
131+
if !self.incomplete.is_empty() {
132+
let mut state = State {
133+
leftover: String::new(),
134+
references: self.state.references.clone(),
135+
highlighter: None,
136+
};
137+
138+
self.incomplete.retain(|index, section| {
139+
if self.items.len() <= *index {
140+
return false;
141+
}
142+
143+
let broken_links_before = section.broken_links.len();
144+
145+
section
146+
.broken_links
147+
.retain(|link| !self.state.references.contains_key(link));
148+
149+
if broken_links_before != section.broken_links.len() {
150+
if let Some((item, _source, _broken_links)) =
151+
parse_with(&mut state, &section.content).next()
152+
{
153+
self.items[*index] = item;
154+
}
155+
}
156+
157+
!section.broken_links.is_empty()
158+
});
159+
}
108160
}
109161

110162
/// Returns the Markdown items, ready to be rendered.
@@ -285,11 +337,13 @@ impl Span {
285337
/// ```
286338
pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
287339
parse_with(State::default(), markdown)
340+
.map(|(item, _source, _broken_links)| item)
288341
}
289342

290343
#[derive(Debug, Default)]
291344
struct State {
292345
leftover: String,
346+
references: HashMap<String, String>,
293347
#[cfg(feature = "highlighter")]
294348
highlighter: Option<Highlighter>,
295349
}
@@ -379,12 +433,14 @@ impl Highlighter {
379433
fn parse_with<'a>(
380434
mut state: impl BorrowMut<State> + 'a,
381435
markdown: &'a str,
382-
) -> impl Iterator<Item = Item> + 'a {
436+
) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
383437
struct List {
384438
start: Option<u64>,
385439
items: Vec<Vec<Item>>,
386440
}
387441

442+
let broken_links = Rc::new(RefCell::new(HashSet::new()));
443+
388444
let mut spans = Vec::new();
389445
let mut code = Vec::new();
390446
let mut strong = false;
@@ -398,14 +454,40 @@ fn parse_with<'a>(
398454
#[cfg(feature = "highlighter")]
399455
let mut highlighter = None;
400456

401-
let parser = pulldown_cmark::Parser::new_ext(
457+
let parser = pulldown_cmark::Parser::new_with_broken_link_callback(
402458
markdown,
403459
pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS
404460
| pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS
405461
| pulldown_cmark::Options::ENABLE_TABLES
406462
| pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
407-
)
408-
.into_offset_iter();
463+
{
464+
let references = state.borrow().references.clone();
465+
let broken_links = broken_links.clone();
466+
467+
Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| {
468+
if let Some(reference) =
469+
references.get(broken_link.reference.as_ref())
470+
{
471+
Some((
472+
pulldown_cmark::CowStr::from(reference.to_owned()),
473+
broken_link.reference.into_static(),
474+
))
475+
} else {
476+
let _ = RefCell::borrow_mut(&broken_links)
477+
.insert(broken_link.reference.to_string());
478+
479+
None
480+
}
481+
})
482+
},
483+
);
484+
485+
let references = &mut state.borrow_mut().references;
486+
487+
for reference in parser.reference_definitions().iter() {
488+
let _ = references
489+
.insert(reference.0.to_owned(), reference.1.dest.to_string());
490+
}
409491

410492
let produce = move |state: &mut State,
411493
lists: &mut Vec<List>,
@@ -414,7 +496,11 @@ fn parse_with<'a>(
414496
if lists.is_empty() {
415497
state.leftover = markdown[source.start..].to_owned();
416498

417-
Some(item)
499+
Some((
500+
item,
501+
&markdown[source.start..source.end],
502+
broken_links.take(),
503+
))
418504
} else {
419505
lists
420506
.last_mut()
@@ -428,6 +514,8 @@ fn parse_with<'a>(
428514
}
429515
};
430516

517+
let parser = parser.into_offset_iter();
518+
431519
// We want to keep the `spans` capacity
432520
#[allow(clippy::drain_collect)]
433521
parser.filter_map(move |(event, source)| match event {

0 commit comments

Comments
 (0)