Merge pull request #17 from gyoder/footnotes

Added footnote support. Closes #16
This commit is contained in:
Quin Gillespie 2025-07-12 23:51:47 -06:00 committed by GitHub
commit 2991b10598
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,9 +5,10 @@ use std::{
rc::Rc, rc::Rc,
}; };
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use kuchikiki::{NodeRef, traits::TendrilSink}; use kuchikiki::{NodeRef, traits::TendrilSink};
use mlua::Lua; use markup5ever::{LocalName, Namespace, QualName};
use mlua::{Lua, Table};
use pulldown_cmark::{Options, Parser, html}; use pulldown_cmark::{Options, Parser, html};
use syntect::{ use syntect::{
easy::HighlightLines, easy::HighlightLines,
@ -180,6 +181,29 @@ pub fn process_syntax_highlighting(document: NodeRef) -> Result<NodeRef> {
Ok(document) Ok(document)
} }
pub fn generate_footnotes(document: NodeRef) -> Result<NodeRef> {
let Ok(footnote_container) = document.select_first("footnotecontainer") else { return Ok(document) };
let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div"));
for (i, footnote) in document.select("footnote").map_err(|()| anyhow!("Failed to get footnote"))?.enumerate() {
let i = i + 1;
let fn_text = footnote.text_contents();
let sup_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one(
format!("<a href=#ft-text-{i}><sup id=\"ft-sup-{i}\" title=\"{fn_text}\">{i}</sup></a>")
).select_first("a").map_err(|()| anyhow!("parse err"))?;
footnote.as_node().insert_after(sup_tag.as_node().clone());
let text_tag = kuchikiki::parse_fragment(ctx_name.clone(), Vec::new()).one(
format!("<p id=\"ft-text-{i}\"><a href=#ft-sup-{i}>{i}:</a> {fn_text}</p>")
).select_first("p").map_err(|()| anyhow!("parse err"))?;
footnote_container.as_node().insert_before(text_tag.as_node().clone());
}
while let Ok(i) = document.select_first("footnote") {
i.as_node().detach();
}
footnote_container.as_node().detach();
Ok(document)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use httptest::{Expectation, ServerPool, matchers::*, responders::*}; use httptest::{Expectation, ServerPool, matchers::*, responders::*};
@ -372,6 +396,46 @@ mod tests {
assert!(d.select_first("includeelement").is_err()); assert!(d.select_first("includeelement").is_err());
} }
#[test]
fn footnotes() {
let page = r"
<!DOCTYPE html>
<html>
<head>
<title>Basic HTML Page</title>
</head>
<body>
<div>
<p>asdf</p>
<p>asdf<footnote>um actually</footnote></p>
<p>asdf</p>
<p>asdf<footnote>no</footnote></p>
</div>
<div>
<footnotecontainer></footnotecontainer>
</div>
</body>
</html>";
let document = kuchikiki::parse_html().one(page);
let d = generate_footnotes(document).unwrap();
let sup1 = d.select_first("#ft-sup-1").unwrap();
assert_eq!(sup1.attributes.borrow().get("title").unwrap(), "um actually");
assert_eq!(sup1.text_contents(), "1");
let sup2 = d.select_first("#ft-sup-2").unwrap();
assert_eq!(sup2.attributes.borrow().get("title").unwrap(), "no");
assert_eq!(sup2.text_contents(), "2");
let ft_text_1 = d.select_first("#ft-text-1").unwrap();
assert!(ft_text_1.text_contents().contains("um actually"));
let ft_text_2 = d.select_first("#ft-text-2").unwrap();
assert!(ft_text_2.text_contents().contains("no"));
assert!(d.select_first("footnote").is_err());
assert!(d.select_first("footnotecontainer").is_err());
}
#[test] #[test]
fn basic_markdown() { fn basic_markdown() {
let page = r" let page = r"