diff --git a/site_test/css/main.scss b/site_test/css/main.scss
index b4f4772..f4b1d17 100644
--- a/site_test/css/main.scss
+++ b/site_test/css/main.scss
@@ -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 {
- text-decoration: none;
+.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;
}
}
diff --git a/site_test/layout/default.html b/site_test/layout/default.html
index db3f1c0..ed0b8f9 100644
--- a/site_test/layout/default.html
+++ b/site_test/layout/default.html
@@ -52,7 +52,7 @@
Archive
Contact
- Generated on {{ _generated_at | pretty_date }} by v7.
+ Generated on {{ _generated_at | pretty_date }}, by v7.
diff --git a/src/generator/markdown.rs b/src/generator/markdown.rs
index 37a5e3e..138b177 100644
--- a/src/generator/markdown.rs
+++ b/src/generator/markdown.rs
@@ -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- > {
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
}
diff --git a/src/generator/markdown/footnote_backrefs.rs b/src/generator/markdown/footnote_backrefs.rs
deleted file mode 100644
index d4f43ba..0000000
--- a/src/generator/markdown/footnote_backrefs.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
-use std::iter::Peekable;
-
-pub struct FootnoteBackrefs<'a, I: Iterator
- >> {
- iter: Peekable,
- next: Option>,
- last_footnote_definition_start: Option>,
-}
-
-pub fn new<'a, I: Iterator
- >>(iter: I) -> FootnoteBackrefs<'a, I> {
- FootnoteBackrefs {
- iter: iter.peekable(),
- next: None,
- last_footnote_definition_start: None,
- }
-}
-
-impl<'a, I: Iterator
- >> Iterator for FootnoteBackrefs<'a, I> {
- type Item = Event<'a>;
-
- fn next(&mut self) -> Option {
- if let Some(e) = self.next.take() {
- return Some(e);
- }
-
- match self.iter.next() {
- Some(Event::FootnoteReference(label)) => {
- let html = format!(
- r##""##,
- 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##" "##,
- 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##"
foo
-
-"##
- );
- }
-}
diff --git a/src/generator/markdown/footnote_defs.rs b/src/generator/markdown/footnote_defs.rs
deleted file mode 100644
index 5cff2d5..0000000
--- a/src/generator/markdown/footnote_defs.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
-use std::collections::VecDeque;
-
-pub fn new<'a, I: Iterator- >>(iter: I) -> FootnoteDefs<'a, I> {
- FootnoteDefs {
- iter,
- footnote_events: VecDeque::new(),
- has_started_emitting_defs: false,
- }
-}
-
-pub struct FootnoteDefs<'a, I: Iterator
- >> {
- iter: I,
- footnote_events: VecDeque>,
- has_started_emitting_defs: bool,
-}
-
-impl<'a, I: Iterator
- >> Iterator for FootnoteDefs<'a, I> {
- type Item = Event<'a>;
-
- fn next(&mut self) -> Option {
- match self.iter.next() {
- Some(Event::Start(Tag::FootnoteDefinition(ref id))) => {
- self.footnote_events.push_back(Event::Html(
- format!(
- r#"".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#"".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##"
foo
-baz
-"##
- );
- }
-
- #[test]
- fn test_multiple() {
- assert_eq!(
- render("foo[^1] bar[^2]\n\n[^1]: foo\n\n[^2]: bar"),
- r##"foo bar
-"##
- );
- }
-}
diff --git a/src/generator/markdown/sidenotes.rs b/src/generator/markdown/sidenotes.rs
new file mode 100644
index 0000000..d9c0da7
--- /dev/null
+++ b/src/generator/markdown/sidenotes.rs
@@ -0,0 +1,306 @@
+use std::collections::{HashMap, HashSet, VecDeque};
+
+use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
+
+pub fn new<'a>(iter: impl Iterator- >) -> impl Iterator
- > {
+ 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>,
+ definition_order: VecDeque>,
+ definitions: HashMap, Vec>>,
+}
+
+enum AdjoinState<'a> {
+ Idle,
+ EmittingDefinition(std::vec::IntoIter>),
+}
+
+impl<'a> AdjoinFootnoteDefinitions<'a> {
+ fn new(mut inner: impl Iterator
- >) -> 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 {
+ 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
- >> {
+ inner: I,
+ state: ReplaceState<'a>,
+ seen_references: HashSet>,
+}
+
+#[derive(Clone)]
+enum ReplaceState<'a> {
+ WaitingForReference,
+ SawReference(CowStr<'a>),
+ InsideSidenote,
+ InsideFootnote(CowStr<'a>),
+ EmitNext(Event<'a>, Box>),
+}
+
+impl<'a, I: Iterator
- >> InsertSidenoteContainers<'a, I> {
+ fn new(inner: I) -> Self {
+ Self {
+ inner,
+ state: ReplaceState::WaitingForReference,
+ seen_references: HashSet::new(),
+ }
+ }
+}
+
+impl<'a, I: Iterator
- >> Iterator for InsertSidenoteContainers<'a, I> {
+ type Item = Event<'a>;
+
+ fn next(&mut self) -> Option {
+ 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##""##).into()))
+ }
+ Some(Event::Start(Tag::FootnoteDefinition(id))) => {
+ self.state = InsideFootnote(id.clone());
+ Some(Event::Html(
+ format!(
+ r#"")),
+ 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> {
+ 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> {
+ 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> {
+ 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##""##.into()),
+ Html(r#""#.into()),
+ Html("".into()),
+ Text("blah".into()),
+ Html("".into()),
+ Html("".into()),
+ End(TagEnd::Paragraph),
+ Start(Tag::Paragraph),
+ Text("weee".into()),
+ End(TagEnd::Paragraph),
+ Html(
+ r#"".into()),
+ ])
+ }
+}