Skip to content

Commit

Permalink
Handle footnote names that have been parsed
Browse files Browse the repository at this point in the history
into multiple nodes. For example `[^_foo]`
gives `^`, `_`, and `foo`.
  • Loading branch information
digitalmoksha committed Jun 1, 2023
1 parent a87860c commit 174ef95
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 23 deletions.
6 changes: 3 additions & 3 deletions src/arena_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A DOM-like tree data structure based on `&Node` references.
Any non-trivial tree involves reference cycles
(e.g. if a node has a first child, the parent of the child is that node).
To enable this, nodes need to live in an arena allocator
such as `arena::TypedArena` distrubuted with rustc (which is `#[unstable]` as of this writing)
such as `arena::TypedArena` distributed with rustc (which is `#[unstable]` as of this writing)
or [`typed_arena::Arena`](https://crates.io/crates/typed-arena).
If you need mutability in the node’s `data`,
Expand All @@ -33,7 +33,7 @@ pub struct Node<'a, T: 'a> {
}

/// A simple Debug implementation that prints the children as a tree, without
/// ilooping through the various interior pointer cycles.
/// looping through the various interior pointer cycles.
impl<'a, T: 'a> fmt::Debug for Node<'a, T>
where
T: fmt::Debug,
Expand Down Expand Up @@ -95,7 +95,7 @@ impl<'a, T> Node<'a, T> {
self.previous_sibling.get()
}

/// Return a reference to the previous sibling of this node, unless it is a last child.
/// Return a reference to the next sibling of this node, unless it is a last child.
pub fn next_sibling(&self) -> Option<&'a Node<'a, T>> {
self.next_sibling.get()
}
Expand Down
63 changes: 43 additions & 20 deletions src/parser/inlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,18 +1216,40 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
return None;
}

let mut text: Option<String> = None;
let bracket_inl_text = self.brackets[brackets_len - 1].inl_text;

if self.options.extension.footnotes
&& match self.brackets[brackets_len - 1].inl_text.next_sibling() {
&& match bracket_inl_text.next_sibling() {
Some(n) => {
text = n.data.borrow().value.text().cloned();
text.is_some() && n.next_sibling().is_none()
if n.data.borrow().value.text().is_some() {
n.data.borrow().value.text().unwrap().as_bytes()[0] == b'^'
} else {
false
}
}
_ => false,
}
{
let text = text.unwrap();
if text.len() > 1 && text.as_bytes()[0] == b'^' {
let mut text = String::new();
let mut sibling_iterator = bracket_inl_text.following_siblings();

// Skip the initial node, which holds the `[`
sibling_iterator.next().unwrap();

// The footnote name could have been parsed into multiple text/htmlinline nodes.
// For example `[^_foo]` gives `^`, `_`, and `foo`. So pull them together.
// Since we're handling the closing bracket, the only siblings at this point are
// related to the footnote name.
for sibling in sibling_iterator {
match sibling.data.borrow().value {
NodeValue::Text(ref literal) | NodeValue::HtmlInline(ref literal) => {
text.push_str(literal);
}
_ => {}
};
}

if text.len() > 1 {
let inl = self.make_inline(
NodeValue::FootnoteReference(NodeFootnoteReference {
name: text[1..].to_string(),
Expand All @@ -1238,24 +1260,25 @@ impl<'a, 'r, 'o, 'd, 'i, 'c, 'subj> Subject<'a, 'r, 'o, 'd, 'i, 'c, 'subj> {
self.pos,
self.pos,
);
inl.data.borrow_mut().sourcepos.start.column = self.brackets[brackets_len - 1]
.inl_text
.data
.borrow()
.sourcepos
.start
.column;
inl.data.borrow_mut().sourcepos.start.column =
bracket_inl_text.data.borrow().sourcepos.start.column;
inl.data.borrow_mut().sourcepos.end.column = usize::try_from(
self.pos as isize + self.column_offset + self.block_offset as isize,
)
.unwrap();
self.brackets[brackets_len - 1].inl_text.insert_before(inl);
self.brackets[brackets_len - 1]
.inl_text
.next_sibling()
.unwrap()
.detach();
self.brackets[brackets_len - 1].inl_text.detach();
bracket_inl_text.insert_before(inl);

// detach all the nodes, including bracket_inl_text
sibling_iterator = bracket_inl_text.following_siblings();
for sibling in sibling_iterator {
match sibling.data.borrow().value {
NodeValue::Text(_) | NodeValue::HtmlInline(_) => {
sibling.detach();
}
_ => {}
};
}

self.process_emphasis(self.brackets[brackets_len - 1].position);
self.brackets.pop();
return None;
Expand Down
54 changes: 54 additions & 0 deletions src/tests/footnotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,60 @@ fn footnote_case_insensitive_and_case_preserving() {
);
}

#[test]
fn footnote_name_parsed_into_multiple_nodes() {
html_opts!(
[extension.footnotes],
concat!(
"Foo.[^_ab]\n",
"\n",
"[^_ab]: Here is the footnote.\n",
),
concat!(
"<p>Foo.<sup class=\"footnote-ref\"><a href=\"#fn-_ab\" id=\"fnref-_ab\" data-footnote-ref>1</a></sup></p>\n",
"<section class=\"footnotes\" data-footnotes>\n",
"<ol>\n",
"<li id=\"fn-_ab\">\n",
"<p>Here is the footnote. <a href=\"#fnref-_ab\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n",
"</li>\n",
"</ol>\n",
"</section>\n"
),
);
}

#[test]
fn footnote_invalid_with_missing_name() {
html_opts!(
[extension.footnotes],
concat!(
"Foo.[^]\n",
"\n",
"[^]: Here is the footnote.\n",
),
concat!(
"<p>Foo.[^]</p>\n",
"<p>[^]: Here is the footnote.</p>\n"
),
);
}

#[test]
fn footnote_does_not_allow_spaces_in_name() {
html_opts!(
[extension.footnotes],
concat!(
"Foo.[^one two]\n",
"\n",
"[^one two]: Here is the footnote.\n",
),
concat!(
"<p>Foo.[^one two]</p>\n",
"<p>[^one two]: Here is the footnote.</p>\n"
),
);
}

#[test]
fn sourcepos() {
assert_ast_match!(
Expand Down

0 comments on commit 174ef95

Please sign in to comment.