Skip to content

Instantly share code, notes, and snippets.

@ollpu
Last active June 5, 2025 09:40
Show Gist options
  • Save ollpu/35c4c4108446bb27f21722dbdbf5384e to your computer and use it in GitHub Desktop.
Save ollpu/35c4c4108446bb27f21722dbdbf5384e to your computer and use it in GitHub Desktop.
pulldown-cmark inline footnotes adapter POC
use std::io::Read;
use pulldown_cmark::{BrokenLink, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd};
/// Collects events nested inside a pair of corresponding Start and End events
///
/// Assumes the Start event is already consumed. The End event will be consumed and discarded.
fn take_until_tag_end<'a>(
events: impl Iterator<Item = Event<'a>>,
) -> impl Iterator<Item = Event<'a>> {
let mut nest = 0i64;
events.take_while(move |event| {
match event {
Event::Start(_) => nest += 1,
Event::End(_) => {
if nest == 0 {
return false;
}
nest -= 1;
}
_ => {}
}
true
})
}
fn handle_inline_footnotes<'a>(
events: impl Iterator<Item = Event<'a>>,
) -> impl Iterator<Item = Event<'a>> {
let mut definitions = Vec::new();
let mut events = events.peekable();
let mut counter = 1;
std::iter::from_fn(move || {
let event = events.next();
if let Some(Event::Text(text)) = &event {
if let Some(text_strip) = text.strip_suffix('^') {
if matches!(
events.peek(),
Some(Event::Start(Tag::Link {
link_type: LinkType::ShortcutUnknown,
..
}))
) {
let _ = events.next();
// This could be anything, but make sure it's unique...
let name = CowStr::from(format!("inl{counter}"));
counter += 1;
definitions.push(Event::Start(Tag::FootnoteDefinition(name.clone())));
// TODO: Wrap in Paragraph so it matches normal footnotes?
definitions.extend(take_until_tag_end(&mut events));
definitions.push(Event::End(TagEnd::FootnoteDefinition));
return Some(vec![
Event::Text(CowStr::from(text_strip).into_static()),
Event::FootnoteReference(name),
]);
}
}
}
if let Some(event) = event {
Some(vec![event])
} else if !definitions.is_empty() {
// Emit the definitions at the end
Some(std::mem::take(&mut definitions))
} else {
None
}
})
.flatten()
}
fn main() {
let mut text = String::new();
std::io::stdin().read_to_string(&mut text).unwrap();
let cb = |link: BrokenLink| {
// TODO: Preserve [ and ] if this doesn't end up being recognized as a footnote
if link.link_type == LinkType::Shortcut {
Some(("".into(), "".into()))
} else {
None
}
};
let parser = Parser::new_with_broken_link_callback(&text, Options::ENABLE_FOOTNOTES, Some(cb));
let parser = handle_inline_footnotes(parser);
pulldown_cmark::html::write_html_io(&mut std::io::stdout().lock(), parser).unwrap();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment