Footnotes/sidenotes
This commit is contained in:
parent
1279a1755d
commit
494f2ad367
@ -1,25 +1,40 @@
|
||||
@import "normalize.scss";
|
||||
@import "fonts.scss";
|
||||
|
||||
$page-horizontal-margin: 2rem;
|
||||
$mobile-breakpoint: 480px;
|
||||
$container-max-width: 768px;
|
||||
:root {
|
||||
--background-color: #f8e7cf;
|
||||
--text-color: black;
|
||||
--secondary-text-color: #656565;
|
||||
--link-color: blue;
|
||||
--page-vertical-margin: 3rem;
|
||||
--page-horizontal-margin: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 768px;
|
||||
max-width: $container-max-width;
|
||||
margin: 0 $page-horizontal-margin / 2;
|
||||
}
|
||||
|
||||
@media (min-width: calc($container-max-width + 2 * $page-horizontal-margin)) {
|
||||
.container {
|
||||
margin: 0 $page-horizontal-margin;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--link-color);
|
||||
text-decoration-thickness: 0.05em;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
/* color: white;
|
||||
text-decoration: none;
|
||||
background-color: black;
|
||||
padding: 0 4px; */
|
||||
}
|
||||
a:hover {
|
||||
text-decoration-thickness: 0.1em;
|
||||
text-decoration-thickness: 3px;
|
||||
}
|
||||
a[href^="http://"]::after,
|
||||
a[href^="https://"]::after
|
||||
@ -37,9 +52,23 @@ a[href^="https://"]::after
|
||||
pre,
|
||||
code {
|
||||
font-family: "Berkeley Mono";
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
body {
|
||||
blockquote {
|
||||
position: relative;
|
||||
font-style: italic;
|
||||
|
||||
&::before {
|
||||
content: open-quote;
|
||||
font-size: 2em;
|
||||
position: absolute;
|
||||
left: -25px;
|
||||
top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Valkyrie A", Charter, serif;
|
||||
font-size: 16px;
|
||||
/* background-color: #dfd3c3; */
|
||||
@ -51,24 +80,43 @@ body {
|
||||
header {
|
||||
margin-top: var(--page-vertical-margin);
|
||||
font-style: italic;
|
||||
|
||||
h1 {
|
||||
font-size: 5rem;
|
||||
margin: 0;
|
||||
text-underline-offset: 0.5rem;
|
||||
a {
|
||||
color: var(--text-color) !important;
|
||||
transition: text-decoration-thickness 0.1s ease-in-out;
|
||||
text-decoration-thickness: 4px;
|
||||
text-underline-offset: 0.5rem;
|
||||
&:hover {
|
||||
text-decoration-thickness: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: lighter;
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-breakpoint) {
|
||||
header h1 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.article-title {
|
||||
// balance the number of words per line
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
font-size: 1.25rem;
|
||||
// Chrome only, but minimizes orphan words
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.header-anchor {
|
||||
@ -77,14 +125,84 @@ header {
|
||||
vertical-align: middle;
|
||||
// drag it up so it's more in the middle
|
||||
padding-bottom: 0.25rem;
|
||||
color: gray !important;
|
||||
color: var(--secondary-text-color) !important;
|
||||
background: none;
|
||||
&:hover {
|
||||
color: var(--link-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-reference > a {
|
||||
.footnote-reference {
|
||||
> a {
|
||||
text-decoration: none;
|
||||
|
||||
&[href^="#fnref-"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover + .sidenote {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenote {
|
||||
float: right;
|
||||
margin-right: -50%;
|
||||
width: 40%;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
color: var(--secondary-text-color);
|
||||
transition: color 0.2s ease-in-out;
|
||||
display: none;
|
||||
|
||||
.sidenote-p {
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
font-size: 1rem;
|
||||
|
||||
.footnote-marker {
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> div > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
> div > p:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5 to account for -50% margin-right on .sidenote
|
||||
// plus page-horizontal-margin to effectively use that as the margin
|
||||
@media (min-width: calc(1.5 * $container-max-width + 2 * $page-horizontal-margin)) {
|
||||
.footnote-reference {
|
||||
> a[href^="#fn-"] {
|
||||
display: none;
|
||||
}
|
||||
> a[href^="#fnref-"] {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.sidenote {
|
||||
display: block;
|
||||
}
|
||||
.footnote {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -97,9 +215,8 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: calc(768px + 2rem)) {
|
||||
.container {
|
||||
margin-left: var(--page-horizontal-margin);
|
||||
margin-right: var(--page-horizontal-margin);
|
||||
@media (max-width: $mobile-breakpoint) {
|
||||
footer {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
<li><a href="/archive/">Archive</a></li>
|
||||
<li><a href="/elsewhere/">Contact</a></li>
|
||||
<!-- TODO: webring -->
|
||||
<li>Generated on {{ _generated_at | pretty_date }} by <a href="https://git.shadowfacts.net/shadowfacts/v7">v7</a>.</li>
|
||||
<li>Generated on {{ _generated_at | pretty_date }}, by <a href="https://git.shadowfacts.net/shadowfacts/v7">v7</a>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,7 +1,6 @@
|
||||
mod footnote_backrefs;
|
||||
mod footnote_defs;
|
||||
mod heading_anchors;
|
||||
mod highlight;
|
||||
mod sidenotes;
|
||||
|
||||
use pulldown_cmark::{Event, Options, Parser, html};
|
||||
use std::io::Write;
|
||||
@ -19,11 +18,8 @@ pub fn parse<'a>(s: &'a str) -> impl Iterator<Item = Event<'a>> {
|
||||
let parser = Parser::new_ext(s, options);
|
||||
// TODO: revisit which of these stages are necessary, remove unused (and url crate dep)
|
||||
let heading_anchors = heading_anchors::new(parser);
|
||||
// note backrefs need to come before defs, because the defs stage replaces the
|
||||
// Tag::FootnoteDefinition events that the backrefs stage relies on with plain html
|
||||
let footnote_backrefs = footnote_backrefs::new(heading_anchors);
|
||||
let footnote_defs = footnote_defs::new(footnote_backrefs);
|
||||
let highlight = highlight::new(footnote_defs);
|
||||
let sidenotes = sidenotes::new(heading_anchors);
|
||||
let highlight = highlight::new(sidenotes);
|
||||
highlight
|
||||
}
|
||||
|
||||
|
@ -1,83 +0,0 @@
|
||||
use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
|
||||
use std::iter::Peekable;
|
||||
|
||||
pub struct FootnoteBackrefs<'a, I: Iterator<Item = Event<'a>>> {
|
||||
iter: Peekable<I>,
|
||||
next: Option<Event<'a>>,
|
||||
last_footnote_definition_start: Option<CowStr<'a>>,
|
||||
}
|
||||
|
||||
pub fn new<'a, I: Iterator<Item = Event<'a>>>(iter: I) -> FootnoteBackrefs<'a, I> {
|
||||
FootnoteBackrefs {
|
||||
iter: iter.peekable(),
|
||||
next: None,
|
||||
last_footnote_definition_start: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for FootnoteBackrefs<'a, I> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(e) = self.next.take() {
|
||||
return Some(e);
|
||||
}
|
||||
|
||||
match self.iter.next() {
|
||||
Some(Event::FootnoteReference(label)) => {
|
||||
let html = format!(
|
||||
r##"<sup class="footnote-reference" id="fnref{}"><a href="#{}">[{}]</a></sup>"##,
|
||||
label, label, label
|
||||
);
|
||||
Some(Event::Html(CowStr::Boxed(html.into_boxed_str())))
|
||||
}
|
||||
Some(Event::Start(Tag::FootnoteDefinition(label))) => {
|
||||
self.last_footnote_definition_start = Some(label.clone());
|
||||
Some(Event::Start(Tag::FootnoteDefinition(label)))
|
||||
}
|
||||
Some(Event::End(TagEnd::Paragraph)) => {
|
||||
if let Some(Event::End(TagEnd::FootnoteDefinition)) = self.iter.peek() {
|
||||
assert!(self.next.is_none());
|
||||
self.next = Some(Event::End(TagEnd::Paragraph));
|
||||
let label = self
|
||||
.last_footnote_definition_start
|
||||
.take()
|
||||
.expect("footnote definition must have started before ending");
|
||||
let html = format!(
|
||||
r##" <a href="#fnref{}" class="footnote-backref">↩</a>"##,
|
||||
label
|
||||
);
|
||||
Some(Event::Html(CowStr::Boxed(html.into_boxed_str())))
|
||||
} else {
|
||||
Some(Event::End(TagEnd::Paragraph))
|
||||
}
|
||||
}
|
||||
e => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
|
||||
fn render(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let parser = Parser::new_ext(s, Options::ENABLE_FOOTNOTES);
|
||||
let footnote_backrefs = super::new(parser);
|
||||
html::push_html(&mut out, footnote_backrefs);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_footnote_backrefs() {
|
||||
assert_eq!(
|
||||
render("foo[^1]\n\n[^1]: bar"),
|
||||
r##"<p>foo<sup class="footnote-reference" id="fnref1"><a href="#1">[1]</a></sup></p>
|
||||
<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>
|
||||
<p>bar <a href="#fnref1" class="footnote-backref">↩</a></p>
|
||||
</div>
|
||||
"##
|
||||
);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub fn new<'a, I: Iterator<Item = Event<'a>>>(iter: I) -> FootnoteDefs<'a, I> {
|
||||
FootnoteDefs {
|
||||
iter,
|
||||
footnote_events: VecDeque::new(),
|
||||
has_started_emitting_defs: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FootnoteDefs<'a, I: Iterator<Item = Event<'a>>> {
|
||||
iter: I,
|
||||
footnote_events: VecDeque<Event<'a>>,
|
||||
has_started_emitting_defs: bool,
|
||||
}
|
||||
|
||||
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for FootnoteDefs<'a, I> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.iter.next() {
|
||||
Some(Event::Start(Tag::FootnoteDefinition(ref id))) => {
|
||||
self.footnote_events.push_back(Event::Html(
|
||||
format!(
|
||||
r#"<div id="{}" class="footnote-item"><span class="footnote-marker">{}.</span>"#,
|
||||
id, id
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
|
||||
loop {
|
||||
match self.iter.next() {
|
||||
Some(Event::End(TagEnd::FootnoteDefinition)) => {
|
||||
self.footnote_events.push_back(Event::Html("</div>".into()));
|
||||
break;
|
||||
}
|
||||
Some(e) => {
|
||||
self.footnote_events.push_back(e);
|
||||
}
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.next()
|
||||
}
|
||||
Some(e) => Some(e),
|
||||
None => {
|
||||
if !self.has_started_emitting_defs && !self.footnote_events.is_empty() {
|
||||
self.has_started_emitting_defs = true;
|
||||
let before: CowStr<'a> =
|
||||
r#"<hr class="footnotes-sep"><section class="footnotes">"#.into();
|
||||
self.footnote_events.push_front(Event::Html(before));
|
||||
let after: CowStr<'a> = "</section>".into();
|
||||
self.footnote_events.push_back(Event::Html(after));
|
||||
}
|
||||
self.footnote_events.pop_front()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
|
||||
fn render(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let parser = Parser::new_ext(s, Options::ENABLE_FOOTNOTES);
|
||||
let footnote_backrefs = super::new(parser);
|
||||
html::push_html(&mut out, footnote_backrefs);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_footnotes() {
|
||||
assert_eq!(
|
||||
render("foo[^1]\n\n[^1]: bar\n\nbaz"),
|
||||
r##"<p>foo<sup class="footnote-reference"><a href="#1">1</a></sup></p>
|
||||
<p>baz</p>
|
||||
<hr class="footnotes-sep"><section class="footnotes"><div id="1" class="footnote-item"><span class="footnote-marker">1.</span>
|
||||
<p>bar</p>
|
||||
</div></section>"##
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple() {
|
||||
assert_eq!(
|
||||
render("foo[^1] bar[^2]\n\n[^1]: foo\n\n[^2]: bar"),
|
||||
r##"<p>foo<sup class="footnote-reference"><a href="#1">1</a></sup> bar<sup class="footnote-reference"><a href="#2">2</a></sup></p>
|
||||
<hr class="footnotes-sep"><section class="footnotes"><div id="1" class="footnote-item"><span class="footnote-marker">1.</span>
|
||||
<p>foo</p>
|
||||
</div><div id="2" class="footnote-item"><span class="footnote-marker">2.</span>
|
||||
<p>bar</p>
|
||||
</div></section>"##
|
||||
);
|
||||
}
|
||||
}
|
306
src/generator/markdown/sidenotes.rs
Normal file
306
src/generator/markdown/sidenotes.rs
Normal file
@ -0,0 +1,306 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
|
||||
|
||||
pub fn new<'a>(iter: impl Iterator<Item = Event<'a>>) -> impl Iterator<Item = Event<'a>> {
|
||||
let adjoined = AdjoinFootnoteDefinitions::new(iter);
|
||||
let containers = InsertSidenoteContainers::new(adjoined);
|
||||
// InsertFootnoteDefnMarkers::new(containers)
|
||||
containers
|
||||
}
|
||||
|
||||
struct AdjoinFootnoteDefinitions<'a> {
|
||||
state: AdjoinState<'a>,
|
||||
all_events: std::vec::IntoIter<Event<'a>>,
|
||||
definition_order: VecDeque<CowStr<'a>>,
|
||||
definitions: HashMap<CowStr<'a>, Vec<Event<'a>>>,
|
||||
}
|
||||
|
||||
enum AdjoinState<'a> {
|
||||
Idle,
|
||||
EmittingDefinition(std::vec::IntoIter<Event<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> AdjoinFootnoteDefinitions<'a> {
|
||||
fn new(mut inner: impl Iterator<Item = Event<'a>>) -> Self {
|
||||
let mut all_events = vec![];
|
||||
let mut definition_order = VecDeque::new();
|
||||
let mut definitions = HashMap::new();
|
||||
while let Some(event) = inner.next() {
|
||||
match event {
|
||||
Event::Start(Tag::FootnoteDefinition(id)) => {
|
||||
definition_order.push_back(id.clone());
|
||||
let mut defn_events = vec![Event::Start(Tag::FootnoteDefinition(id.clone()))];
|
||||
'defn: while let Some(event) = inner.next() {
|
||||
match event {
|
||||
Event::End(TagEnd::FootnoteDefinition) => {
|
||||
defn_events.push(Event::End(TagEnd::FootnoteDefinition));
|
||||
break 'defn;
|
||||
}
|
||||
_ => defn_events.push(event),
|
||||
}
|
||||
}
|
||||
definitions.insert(id, defn_events);
|
||||
}
|
||||
_ => all_events.push(event),
|
||||
}
|
||||
}
|
||||
Self {
|
||||
state: AdjoinState::Idle,
|
||||
all_events: all_events.into_iter(),
|
||||
definition_order,
|
||||
definitions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AdjoinFootnoteDefinitions<'a> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match &mut self.state {
|
||||
AdjoinState::Idle => match self.all_events.next() {
|
||||
Some(Event::FootnoteReference(id)) => {
|
||||
if let Some(defn) = self.definitions.get(&id) {
|
||||
self.state = AdjoinState::EmittingDefinition(defn.clone().into_iter());
|
||||
}
|
||||
Some(Event::FootnoteReference(id))
|
||||
}
|
||||
Some(e) => Some(e),
|
||||
None => match self.definition_order.pop_front() {
|
||||
Some(id) => {
|
||||
if let Some(defn) = self.definitions.get(&id) {
|
||||
self.state = AdjoinState::EmittingDefinition(defn.clone().into_iter());
|
||||
}
|
||||
self.next()
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
},
|
||||
AdjoinState::EmittingDefinition(defn) => match defn.next() {
|
||||
Some(e) => Some(e),
|
||||
None => {
|
||||
self.state = AdjoinState::Idle;
|
||||
self.next()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InsertSidenoteContainers<'a, I: Iterator<Item = Event<'a>>> {
|
||||
inner: I,
|
||||
state: ReplaceState<'a>,
|
||||
seen_references: HashSet<CowStr<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ReplaceState<'a> {
|
||||
WaitingForReference,
|
||||
SawReference(CowStr<'a>),
|
||||
InsideSidenote,
|
||||
InsideFootnote(CowStr<'a>),
|
||||
EmitNext(Event<'a>, Box<ReplaceState<'a>>),
|
||||
}
|
||||
|
||||
impl<'a, I: Iterator<Item = Event<'a>>> InsertSidenoteContainers<'a, I> {
|
||||
fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
state: ReplaceState::WaitingForReference,
|
||||
seen_references: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for InsertSidenoteContainers<'a, I> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use ReplaceState::*;
|
||||
|
||||
match self.state {
|
||||
WaitingForReference => match self.inner.next() {
|
||||
Some(Event::FootnoteReference(id)) => {
|
||||
assert!(!self.seen_references.contains(&id));
|
||||
self.state = SawReference(id.clone());
|
||||
Some(Event::Html(format!(r##"<sup id="fnref-{id}" class="footnote-reference"><a href="#fn-{id}">[{id}]</a><a href="#fnref-{id}">[{id}]</a></sup>"##).into()))
|
||||
}
|
||||
Some(Event::Start(Tag::FootnoteDefinition(id))) => {
|
||||
self.state = InsideFootnote(id.clone());
|
||||
Some(Event::Html(
|
||||
format!(
|
||||
r#"<div id="fn-{id}" class="footnote"><span class="footnote-marker">{id}. </span><div>"#,
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
maybe_event => maybe_event,
|
||||
},
|
||||
SawReference(ref id) => match self.inner.next() {
|
||||
Some(Event::Start(Tag::FootnoteDefinition(def_id))) => {
|
||||
assert_eq!(id, &def_id);
|
||||
self.seen_references.insert(def_id.clone());
|
||||
self.state = InsideSidenote;
|
||||
Some(Event::Html(format!(
|
||||
r#"<span class="sidenote"><span class="footnote-marker">{def_id}. </span>"#,
|
||||
).into()))
|
||||
}
|
||||
e => panic!("should not receive {e:?} while in SawReference state"),
|
||||
},
|
||||
InsideSidenote => match self.inner.next() {
|
||||
// We drop <p> in sidenote defn because, since the sidenote defn appears inside of a normal paragraph,
|
||||
// the sidenote defn <p> would implicitly close the containing <p> and screw up the hierarchy.
|
||||
Some(Event::Start(Tag::Paragraph)) => Some(Event::Html(CowStr::Borrowed(""))),
|
||||
Some(Event::End(TagEnd::Paragraph)) => Some(Event::Html(CowStr::Borrowed(""))),
|
||||
Some(Event::End(TagEnd::FootnoteDefinition)) => {
|
||||
self.state = WaitingForReference;
|
||||
Some(Event::Html(CowStr::Borrowed("</span>")))
|
||||
}
|
||||
e => e,
|
||||
},
|
||||
InsideFootnote(ref id) => match self.inner.next() {
|
||||
Some(Event::End(TagEnd::Paragraph)) => {
|
||||
// unicode variation selector to make the back arrow always render as text (not emoji)
|
||||
let text_selector = "\u{FE0E}";
|
||||
let event = Event::Html(
|
||||
format!(
|
||||
r##" <a href="#fnref-{id}" class="footnote-backref">↩{text_selector}</a>"##
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
let close_divs = EmitNext(
|
||||
Event::Html(CowStr::Borrowed("</div></div>")),
|
||||
Box::new(InsideFootnote(id.clone())),
|
||||
);
|
||||
let end_para = EmitNext(Event::End(TagEnd::Paragraph), Box::new(close_divs));
|
||||
self.state = end_para;
|
||||
Some(event)
|
||||
}
|
||||
Some(Event::End(TagEnd::FootnoteDefinition)) => {
|
||||
self.state = WaitingForReference;
|
||||
self.next()
|
||||
}
|
||||
maybe_event => maybe_event,
|
||||
},
|
||||
EmitNext(ref event, ref next_state) => {
|
||||
let event = event.clone();
|
||||
self.state = next_state.as_ref().clone();
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pulldown_cmark::{CowStr, Event, Event::*, Options, Parser, Tag, TagEnd};
|
||||
|
||||
fn render_adjoined(s: &'static str) -> Vec<Event<'static>> {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
super::AdjoinFootnoteDefinitions::new(Parser::new_ext(s, options)).collect()
|
||||
}
|
||||
|
||||
fn render_sidenote_containers(s: &'static str) -> Vec<Event<'static>> {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
super::InsertSidenoteContainers::new(super::AdjoinFootnoteDefinitions::new(
|
||||
Parser::new_ext(s, options),
|
||||
))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render(s: &'static str) -> Vec<Event<'static>> {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
super::new(Parser::new_ext(s, options)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjoin_simple() {
|
||||
assert_eq!(render_adjoined("[^1]\n\n[^1]: defn\n\nbar"), vec![
|
||||
Start(Tag::Paragraph),
|
||||
FootnoteReference(CowStr::Borrowed("1")),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("1"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("defn")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("bar")),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("1"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("defn")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition)
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjoin_out_of_order() {
|
||||
assert_eq!(
|
||||
render_adjoined("[^1]\n\n[^2]\n\n[^2]: baz\n\n[^1]: defn\n\nbar"),
|
||||
vec![
|
||||
Start(Tag::Paragraph),
|
||||
FootnoteReference(CowStr::Borrowed("1")),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("1"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("defn")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::Paragraph),
|
||||
FootnoteReference(CowStr::Borrowed("2")),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("2"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("baz")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("bar")),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("2"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("baz")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition),
|
||||
Start(Tag::FootnoteDefinition(CowStr::Borrowed("1"))),
|
||||
Start(Tag::Paragraph),
|
||||
Text(CowStr::Borrowed("defn")),
|
||||
End(TagEnd::Paragraph),
|
||||
End(TagEnd::FootnoteDefinition),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidenote_markers() {
|
||||
assert_eq!(render("look[^1]\n\n[^1]: blah\n\nweee"), vec![
|
||||
Start(Tag::Paragraph),
|
||||
Text("look".into()),
|
||||
Html(r##"<sup id="fnref-1" class="footnote-reference"><a href="#fn-1">[1]</a><a href="#fnref-1">[1]</a></sup>"##.into()),
|
||||
Html(r#"<span class="sidenote"><span class="footnote-marker">1. </span>"#.into()),
|
||||
Html("".into()),
|
||||
Text("blah".into()),
|
||||
Html("".into()),
|
||||
Html("</span>".into()),
|
||||
End(TagEnd::Paragraph),
|
||||
Start(Tag::Paragraph),
|
||||
Text("weee".into()),
|
||||
End(TagEnd::Paragraph),
|
||||
Html(
|
||||
r#"<div id="fn-1" class="footnote"><span class="footnote-marker">1. </span><div>"#
|
||||
.into()
|
||||
),
|
||||
Start(Tag::Paragraph),
|
||||
Text("blah".into()),
|
||||
Html(r##" <a href="#fnref-1" class="footnote-backref">↩</a>"##.into()),
|
||||
End(TagEnd::Paragraph),
|
||||
Html("</div></div>".into()),
|
||||
])
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user