Last active
June 5, 2025 09:40
-
-
Save ollpu/35c4c4108446bb27f21722dbdbf5384e to your computer and use it in GitHub Desktop.
pulldown-cmark inline footnotes adapter POC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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