Templating system
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
9ffac14e2b
commit
10769956cf
|
@ -1,3 +1,7 @@
|
||||||
|
``` =toml
|
||||||
|
created-at: 2024-10-31 15:28
|
||||||
|
```
|
||||||
|
|
||||||
# Why I love arcaea
|
# Why I love arcaea
|
||||||
|
|
||||||
## What is arcaea
|
## What is arcaea
|
||||||
|
@ -32,19 +36,12 @@ Let's write the score formula in a nice, closed form!
|
||||||
|
|
||||||
Let $`m`, $`p`, $`f` and $`l` denote the amount of MAX PURE, PURE, FAR and LOST notes respectively. The final score can then be computed as
|
Let $`m`, $`p`, $`f` and $`l` denote the amount of MAX PURE, PURE, FAR and LOST notes respectively. The final score can then be computed as
|
||||||
|
|
||||||
$$`\left\lfloor (2(m + p) + f) \frac{10'000'000}{2(m + p + f)} \right\rfloor`
|
$$`m + \left\lfloor (2(m + p) + f) \frac{10'000'000}{2(m + p + f + l)} \right\rfloor`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
{.label="Lagrange's extras: ζ-scoring"}
|
{title="Lagrange's extras: ζ-scoring" character="lagrange"}
|
||||||
:::: in-depth
|
::: aside
|
||||||
::: in-depth-header
|
UWAAA, but what if we wanted MAX PURE notes to have a more major contribution to the score? For one, we could start by giving them their own place in the scoring ratio. What would a good ratio look like? A naive approach idea would be to keep the same rate of growth and go with a ratio of 4\:2\:1 for MAX PURE to PURE to FAR. Sadly, issues arise because this can lead to PMs possibly producing terrible scores — it's too big of a departure from the original formula. It turns out the aforementioned [sound voltex](https://en.wikipedia.org/wiki/Sound_Voltex) has already figured out a solution with their optional "EX-scoring" system, which uses a ratio of 5\:4\:2, thus awarding 1.25x the normal points for a MAX PURE.
|
||||||
{.in-depth-icon}
|
|
||||||
|
|
||||||
{.in-depth-heading}
|
|
||||||
### EX scoring
|
|
||||||
:::
|
|
||||||
|
|
||||||
But what if we wanted MAX PURE notes to have a more major contribution to the score? For one, we could start by giving them their own place in the scoring ratio. What would a good ratio look like? A naive approach idea would be to keep the same rate of growth and go with a ratio of 4\:2\:1 for MAX PURE to PURE to FAR. Sadly, issues arise because this can lead to PMs possibly producing terrible scores — it's too big of a departure from the original formula. It turns out the aforementioned [sound voltex](https://en.wikipedia.org/wiki/Sound_Voltex) has already figured out a solution with their optional "EX-scoring" system, which uses a ratio of 5\:4\:2, thus awarding 1.25x the normal points for a MAX PURE.
|
|
||||||
|
|
||||||
Calling this "EX-scoring" in the context of Arcaea would be confusing (EX being an in-game grade and all), hence I decided to call the Arcaea equivalent of the system "ζ-scoring". In particular, we can compute the ζ-score by only knowing the base score and the number of notes in the chart (call it $`n`) as follows:
|
Calling this "EX-scoring" in the context of Arcaea would be confusing (EX being an in-game grade and all), hence I decided to call the Arcaea equivalent of the system "ζ-scoring". In particular, we can compute the ζ-score by only knowing the base score and the number of notes in the chart (call it $`n`) as follows:
|
||||||
|
|
||||||
|
@ -53,7 +50,7 @@ Calling this "EX-scoring" in the context of Arcaea would be confusing (EX being
|
||||||
3. Double the quotient from step 2 and add the remainder to form the final expression of $`5m + 4p + 2f`, which can then be scaled up so $`10,000,000` is the maximum score again.
|
3. Double the quotient from step 2 and add the remainder to form the final expression of $`5m + 4p + 2f`, which can then be scaled up so $`10,000,000` is the maximum score again.
|
||||||
|
|
||||||
With a bit of care put into working around the floor function in the actual scoring formula, and performing the computations in a manner that avoids any risks of floating point arithmetic errors, one can implement a very reliable score converter. For instance, the score tracking Arcaea discord bot I'm developing has ζ-scoring well-integrated into everything!
|
With a bit of care put into working around the floor function in the actual scoring formula, and performing the computations in a manner that avoids any risks of floating point arithmetic errors, one can implement a very reliable score converter. For instance, the score tracking Arcaea discord bot I'm developing has ζ-scoring well-integrated into everything!
|
||||||
:::::
|
:::
|
||||||
{% ]]] %}
|
{% ]]] %}
|
||||||
|
|
||||||
#### Why arcaea's scoring rocks
|
#### Why arcaea's scoring rocks
|
||||||
|
|
|
@ -2,67 +2,75 @@ html {
|
||||||
font: 100%/1.5 sans-serif;
|
font: 100%/1.5 sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#page-content {
|
||||||
|
max-width: 70ch;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
border-left: 3px solid;
|
border-left: 3px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-depth {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-depth-header {
|
/* {{{ Asides*/
|
||||||
|
.aside {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-header {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-depth-header > .in-depth-heading {
|
.aside-summary > .aside-title {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-depth-header > * {
|
.aside-summary > * {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.in-depth-icon {
|
img.aside-icon {
|
||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
.aside {
|
||||||
background: #ead3ed;
|
background: #ead3ed;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0.5rem 0.5rem;
|
padding: 0.5rem 0.5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
.aside-summary {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary::marker {
|
.aside-summary::marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary:before {
|
.aside-summary:before {
|
||||||
content: "▶";
|
content: "▶";
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0 0.75rem 0 0.25rem;
|
padding: 0 0.75rem 0 0.25rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] summary:before {
|
.aside[open] .aside_summary:before {
|
||||||
content: "▼";
|
content: "▼";
|
||||||
}
|
}
|
||||||
|
|
||||||
details > .in-depth {
|
.aside > .aside-content {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
/* }}}*/
|
||||||
|
|
313
src/html.rs
313
src/html.rs
|
@ -2,82 +2,99 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use jotdown::Alignment;
|
use jotdown::Alignment;
|
||||||
use jotdown::Container;
|
use jotdown::Container;
|
||||||
use jotdown::Event;
|
use jotdown::Event;
|
||||||
use jotdown::LinkType;
|
use jotdown::LinkType;
|
||||||
use jotdown::ListKind;
|
use jotdown::ListKind;
|
||||||
use jotdown::OrderedListNumbering::*;
|
use jotdown::OrderedListNumbering::*;
|
||||||
use jotdown::Render;
|
|
||||||
use jotdown::RenderRef;
|
|
||||||
use jotdown::SpanLinkType;
|
use jotdown::SpanLinkType;
|
||||||
|
|
||||||
|
use crate::template;
|
||||||
|
use crate::template::Template;
|
||||||
|
use crate::template::TemplateRenderer;
|
||||||
|
|
||||||
// {{{ Renderer
|
// {{{ Renderer
|
||||||
/// Render events into a string.
|
/// Render events into a string.
|
||||||
pub fn render_to_string<'s, I>(events: I) -> String
|
pub fn render_to_string<'s, I>(events: I) -> anyhow::Result<String>
|
||||||
where
|
where
|
||||||
I: Iterator<Item = Event<'s>>,
|
I: Iterator<Item = Event<'s>>,
|
||||||
{
|
{
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
Renderer::default().push(events, &mut s).unwrap();
|
Renderer::new()?.push(events, &mut s)?;
|
||||||
s
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Render`] implementor that writes HTML output.
|
/// [`Render`] implementor that writes HTML output.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Renderer {}
|
pub struct Renderer {
|
||||||
|
templates: BuiltinTempaltes,
|
||||||
impl Render for Renderer {
|
|
||||||
fn push<'s, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
|
|
||||||
where
|
|
||||||
I: Iterator<Item = Event<'s>>,
|
|
||||||
W: std::fmt::Write,
|
|
||||||
{
|
|
||||||
let mut w = Writer::new();
|
|
||||||
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
|
||||||
w.render_epilogue(&mut out)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderRef for Renderer {
|
impl Renderer {
|
||||||
fn push_ref<'s, E, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
where
|
Ok(Self {
|
||||||
E: AsRef<Event<'s>>,
|
templates: BuiltinTempaltes::new()?,
|
||||||
I: Iterator<Item = E>,
|
})
|
||||||
W: std::fmt::Write,
|
}
|
||||||
{
|
|
||||||
let mut w = Writer::new();
|
pub fn push<'s>(
|
||||||
events.try_for_each(|e| w.render_event(e.as_ref(), &mut out))?;
|
&self,
|
||||||
w.render_epilogue(&mut out)
|
mut events: impl Iterator<Item = Event<'s>>,
|
||||||
|
mut out: impl std::fmt::Write,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut w = Writer::new(self);
|
||||||
|
events.try_for_each(|e| w.render_event(&e, &mut out))?;
|
||||||
|
w.render_epilogue(&mut out)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Bring in templates
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct BuiltinTempaltes {
|
||||||
|
aside_template: Template,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuiltinTempaltes {
|
||||||
|
fn new() -> anyhow::Result<Self> {
|
||||||
|
Ok(BuiltinTempaltes {
|
||||||
|
aside_template: template!("./templates/aside.html")?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
struct Writer<'s> {
|
struct Writer<'s> {
|
||||||
list_tightness: Vec<bool>,
|
list_tightness: Vec<bool>,
|
||||||
states: Vec<State>,
|
states: Vec<State<'s>>,
|
||||||
footnotes: Footnotes<'s>,
|
footnotes: Footnotes<'s>,
|
||||||
|
renderer: &'s Renderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
enum State {
|
enum State<'s> {
|
||||||
TextOnly,
|
TextOnly,
|
||||||
Ignore,
|
Ignore,
|
||||||
Raw,
|
Raw,
|
||||||
Math(bool),
|
Math(bool),
|
||||||
|
Aside(TemplateRenderer<'s>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> Writer<'s> {
|
impl<'s> Writer<'s> {
|
||||||
fn new() -> Self {
|
fn new(renderer: &'s Renderer) -> Self {
|
||||||
Self {
|
Self {
|
||||||
list_tightness: Vec::new(),
|
list_tightness: Vec::new(),
|
||||||
states: Vec::new(),
|
states: Vec::new(),
|
||||||
footnotes: Footnotes::default(),
|
footnotes: Footnotes::default(),
|
||||||
|
renderer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::single_match)]
|
#[allow(clippy::single_match)]
|
||||||
fn render_event<W>(&mut self, e: &Event<'s>, mut out: W) -> std::fmt::Result
|
fn render_event<W>(&mut self, e: &Event<'s>, mut out: W) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
W: std::fmt::Write,
|
W: std::fmt::Write,
|
||||||
{
|
{
|
||||||
|
@ -101,7 +118,7 @@ impl<'s> Writer<'s> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Event::End(Container::LinkDefinition { .. }) => {
|
Event::End(Container::LinkDefinition { .. }) => {
|
||||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||||||
self.states.pop();
|
self.states.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,9 +133,9 @@ impl<'s> Writer<'s> {
|
||||||
}
|
}
|
||||||
Event::End(Container::RawBlock { format } | Container::RawInline { format }) => {
|
Event::End(Container::RawBlock { format } | Container::RawInline { format }) => {
|
||||||
if format == &"html" {
|
if format == &"html" {
|
||||||
assert_eq!(self.states.last(), Some(&State::Raw));
|
assert!(matches!(self.states.last(), Some(State::Raw)));
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||||||
};
|
};
|
||||||
|
|
||||||
self.states.pop();
|
self.states.pop();
|
||||||
|
@ -128,14 +145,14 @@ impl<'s> Writer<'s> {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
if self.states.last() == Some(&State::Ignore) {
|
if matches!(self.states.last(), Some(State::Ignore)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match e {
|
match e {
|
||||||
// {{{ Container start
|
// {{{ Container start
|
||||||
Event::Start(c, attrs) => {
|
Event::Start(c, attrs) => {
|
||||||
if self.states.last() == Some(&State::TextOnly) {
|
if matches!(self.states.last(), Some(&State::TextOnly)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +217,35 @@ impl<'s> Writer<'s> {
|
||||||
Container::Table => out.write_str("<table")?,
|
Container::Table => out.write_str("<table")?,
|
||||||
Container::TableRow { .. } => out.write_str("<tr")?,
|
Container::TableRow { .. } => out.write_str("<tr")?,
|
||||||
Container::Section { .. } => out.write_str("<section")?,
|
Container::Section { .. } => out.write_str("<section")?,
|
||||||
|
Container::Div { class: "aside" } => {
|
||||||
|
let title = attrs.get_value("title").ok_or_else(|| {
|
||||||
|
anyhow!("Cannot find `title` attribute on `aside` element")
|
||||||
|
})?;
|
||||||
|
let character = attrs.get_value("character").ok_or_else(|| {
|
||||||
|
anyhow!("Cannot find `character` attribute on `aside` element")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut renderer =
|
||||||
|
TemplateRenderer::new(&self.renderer.templates.aside_template);
|
||||||
|
|
||||||
|
while let Some(label) = renderer.current(&mut out)? {
|
||||||
|
if label == "character" {
|
||||||
|
character
|
||||||
|
.parts()
|
||||||
|
.try_for_each(|part| write_attr(part, &mut out))?;
|
||||||
|
renderer.next(&mut out)?;
|
||||||
|
} else if label == "title" {
|
||||||
|
title
|
||||||
|
.parts()
|
||||||
|
.try_for_each(|part| write_attr(part, &mut out))?;
|
||||||
|
renderer.next(&mut out)?;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.states.push(State::Aside(renderer));
|
||||||
|
}
|
||||||
Container::Div { .. } => out.write_str("<div")?,
|
Container::Div { .. } => out.write_str("<div")?,
|
||||||
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
Container::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||||||
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
||||||
|
@ -220,85 +266,93 @@ impl<'s> Writer<'s> {
|
||||||
Container::LinkDefinition { .. } => return Ok(()),
|
Container::LinkDefinition { .. } => return Ok(()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Write attributes
|
let mut write_attribs = true;
|
||||||
let mut id_written = false;
|
if matches!(c, Container::Div { class: "aside" }) {
|
||||||
let mut class_written = false;
|
write_attribs = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (a, v) in attrs.unique_pairs() {
|
if write_attribs {
|
||||||
write!(out, r#" {}=""#, a)?;
|
// {{{ Write attributes
|
||||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
let mut id_written = false;
|
||||||
match a {
|
let mut class_written = false;
|
||||||
"class" => {
|
|
||||||
class_written = true;
|
if write_attribs {
|
||||||
write_class(c, true, &mut out)?;
|
for (a, v) in attrs.unique_pairs() {
|
||||||
|
let is_class = a == "class";
|
||||||
|
let is_id = a == "id";
|
||||||
|
if (!is_id || !id_written) && (!is_class || !class_written) {
|
||||||
|
write!(out, r#" {}=""#, a)?;
|
||||||
|
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||||
|
out.write_char('"')?;
|
||||||
|
|
||||||
|
id_written |= is_id;
|
||||||
|
class_written |= is_class;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
"id" => id_written = true,
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
out.write_char('"')?;
|
// }}}
|
||||||
}
|
// {{{ Write default ids/classes
|
||||||
// }}}
|
if let Container::Heading {
|
||||||
// {{{ Write default ids/classes
|
id,
|
||||||
if let Container::Heading {
|
has_section: false,
|
||||||
id,
|
..
|
||||||
has_section: false,
|
}
|
||||||
..
|
| Container::Section { id } = &c
|
||||||
}
|
{
|
||||||
| Container::Section { id } = &c
|
if !id_written {
|
||||||
{
|
out.write_str(r#" id=""#)?;
|
||||||
if !id_written {
|
write_attr(id, &mut out)?;
|
||||||
out.write_str(r#" id=""#)?;
|
out.write_char('"')?;
|
||||||
write_attr(id, &mut out)?;
|
}
|
||||||
|
// TODO: do I not want this to add onto the provided class?
|
||||||
|
} else if (matches!(c, Container::Div { class } if !class.is_empty())
|
||||||
|
|| matches!(
|
||||||
|
c,
|
||||||
|
Container::Math { .. }
|
||||||
|
| Container::List {
|
||||||
|
kind: ListKind::Task(..),
|
||||||
|
..
|
||||||
|
} | Container::TaskListItem { .. }
|
||||||
|
)) && !class_written
|
||||||
|
{
|
||||||
|
out.write_str(r#" class=""#)?;
|
||||||
|
write_class(c, false, &mut out)?;
|
||||||
out.write_char('"')?;
|
out.write_char('"')?;
|
||||||
}
|
}
|
||||||
// TODO: do I not want this to add onto the provided class?
|
// }}}
|
||||||
} else if (matches!(c, Container::Div { class } if !class.is_empty())
|
|
||||||
|| matches!(
|
|
||||||
c,
|
|
||||||
Container::Math { .. }
|
|
||||||
| Container::List {
|
|
||||||
kind: ListKind::Task(..),
|
|
||||||
..
|
|
||||||
} | Container::TaskListItem { .. }
|
|
||||||
)) && !class_written
|
|
||||||
{
|
|
||||||
out.write_str(r#" class=""#)?;
|
|
||||||
write_class(c, false, &mut out)?;
|
|
||||||
out.write_char('"')?;
|
|
||||||
}
|
|
||||||
// }}}
|
|
||||||
|
|
||||||
match c {
|
match c {
|
||||||
// {{{ Write css for aligning table cell text
|
// {{{ Write css for aligning table cell text
|
||||||
Container::TableCell { alignment, .. }
|
Container::TableCell { alignment, .. }
|
||||||
if !matches!(alignment, Alignment::Unspecified) =>
|
if !matches!(alignment, Alignment::Unspecified) =>
|
||||||
{
|
{
|
||||||
let a = match alignment {
|
let a = match alignment {
|
||||||
Alignment::Unspecified => unreachable!(),
|
Alignment::Unspecified => unreachable!(),
|
||||||
Alignment::Left => "left",
|
Alignment::Left => "left",
|
||||||
Alignment::Center => "center",
|
Alignment::Center => "center",
|
||||||
Alignment::Right => "right",
|
Alignment::Right => "right",
|
||||||
};
|
};
|
||||||
write!(out, r#" style="text-align: {};">"#, a)?;
|
write!(out, r#" style="text-align: {};">"#, a)?;
|
||||||
}
|
|
||||||
// }}}
|
|
||||||
// {{{ Write language for codeblock
|
|
||||||
Container::CodeBlock { language } => {
|
|
||||||
if language.is_empty() {
|
|
||||||
out.write_str("><code>")?;
|
|
||||||
} else {
|
|
||||||
out.write_str(r#"><code class="language-"#)?;
|
|
||||||
write_attr(language, &mut out)?;
|
|
||||||
out.write_str(r#"">"#)?;
|
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Write language for codeblock
|
||||||
|
Container::CodeBlock { language } => {
|
||||||
|
if language.is_empty() {
|
||||||
|
out.write_str("><code>")?;
|
||||||
|
} else {
|
||||||
|
out.write_str(r#"><code class="language-"#)?;
|
||||||
|
write_attr(language, &mut out)?;
|
||||||
|
out.write_str(r#"">"#)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
Container::Image(..) => out.write_str(r#" alt=""#)?,
|
||||||
|
Container::Math { display } => {
|
||||||
|
out.write_str(r#">"#)?;
|
||||||
|
self.states.push(State::Math(*display));
|
||||||
|
}
|
||||||
|
_ => out.write_char('>')?,
|
||||||
}
|
}
|
||||||
// }}}
|
|
||||||
Container::Image(..) => out.write_str(r#" alt=""#)?,
|
|
||||||
Container::Math { display } => {
|
|
||||||
out.write_str(r#">"#)?;
|
|
||||||
self.states.push(State::Math(*display));
|
|
||||||
}
|
|
||||||
_ => out.write_char('>')?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match &c {
|
match &c {
|
||||||
|
@ -313,13 +367,13 @@ impl<'s> Writer<'s> {
|
||||||
Event::End(c) => {
|
Event::End(c) => {
|
||||||
match &c {
|
match &c {
|
||||||
Container::Image(..) => {
|
Container::Image(..) => {
|
||||||
assert_eq!(self.states.last(), Some(&State::TextOnly));
|
assert!(matches!(self.states.last(), Some(State::TextOnly)));
|
||||||
self.states.pop();
|
self.states.pop();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.states.last() == Some(&State::TextOnly) {
|
if matches!(self.states.last(), Some(State::TextOnly)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,6 +428,15 @@ impl<'s> Writer<'s> {
|
||||||
Container::Table => out.write_str("</table>")?,
|
Container::Table => out.write_str("</table>")?,
|
||||||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||||||
Container::Section { .. } => out.write_str("</section>")?,
|
Container::Section { .. } => out.write_str("</section>")?,
|
||||||
|
Container::Div { class: "aside" } => {
|
||||||
|
let state = self.states.pop().unwrap();
|
||||||
|
let State::Aside(mut renderer) = state else {
|
||||||
|
panic!("Finished `aside` element without being in the `Aside` state.")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(renderer.current(&mut out)?, Some("content"));
|
||||||
|
renderer.finish(&mut out)?;
|
||||||
|
}
|
||||||
Container::Div { .. } => out.write_str("</div>")?,
|
Container::Div { .. } => out.write_str("</div>")?,
|
||||||
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||||
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
||||||
|
@ -428,7 +491,7 @@ impl<'s> Writer<'s> {
|
||||||
// {{{ Footnote reference
|
// {{{ Footnote reference
|
||||||
Event::FootnoteReference(label) => {
|
Event::FootnoteReference(label) => {
|
||||||
let number = self.footnotes.reference(label);
|
let number = self.footnotes.reference(label);
|
||||||
if self.states.last() != Some(&State::TextOnly) {
|
if !matches!(self.states.last(), Some(State::TextOnly)) {
|
||||||
write!(
|
write!(
|
||||||
out,
|
out,
|
||||||
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
||||||
|
@ -468,10 +531,7 @@ impl<'s> Writer<'s> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Render epilogue
|
// {{{ Render epilogue
|
||||||
fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
|
fn render_epilogue(&mut self, mut out: impl std::fmt::Write) -> anyhow::Result<()> {
|
||||||
where
|
|
||||||
W: std::fmt::Write,
|
|
||||||
{
|
|
||||||
if self.footnotes.reference_encountered() {
|
if self.footnotes.reference_encountered() {
|
||||||
out.write_str("<section role=\"doc-endnotes\">")?;
|
out.write_str("<section role=\"doc-endnotes\">")?;
|
||||||
out.write_str("<hr>")?;
|
out.write_str("<hr>")?;
|
||||||
|
@ -545,24 +605,21 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_text<W>(s: &str, out: W) -> std::fmt::Result
|
#[inline]
|
||||||
where
|
fn write_text(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||||||
W: std::fmt::Write,
|
|
||||||
{
|
|
||||||
write_escape(s, false, out)
|
write_escape(s, false, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_attr<W>(s: &str, out: W) -> std::fmt::Result
|
#[inline]
|
||||||
where
|
fn write_attr(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||||||
W: std::fmt::Write,
|
|
||||||
{
|
|
||||||
write_escape(s, true, out)
|
write_escape(s, true, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_escape<W>(mut s: &str, escape_quotes: bool, mut out: W) -> std::fmt::Result
|
fn write_escape(
|
||||||
where
|
mut s: &str,
|
||||||
W: std::fmt::Write,
|
escape_quotes: bool,
|
||||||
{
|
mut out: impl std::fmt::Write,
|
||||||
|
) -> std::fmt::Result {
|
||||||
let mut ent = "";
|
let mut ent = "";
|
||||||
while let Some(i) = s.find(|c| {
|
while let Some(i) = s.find(|c| {
|
||||||
match c {
|
match c {
|
||||||
|
|
62
src/main.rs
62
src/main.rs
|
@ -1,8 +1,60 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use html::Renderer;
|
||||||
|
use template::TemplateRenderer;
|
||||||
|
|
||||||
mod html;
|
mod html;
|
||||||
|
mod template;
|
||||||
mod tex;
|
mod tex;
|
||||||
fn main() {
|
|
||||||
let djot_input = std::fs::read_to_string("content/arcaea.dj").unwrap();
|
fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||||
let events = jotdown::Parser::new(&djot_input);
|
Command::new("cp").arg("-r").arg(from).arg(to).output()?;
|
||||||
let html = crate::html::render_to_string(events);
|
|
||||||
println!("{html}");
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let dist_path = PathBuf::from_str("dist")?;
|
||||||
|
let public_path = PathBuf::from_str("public")?;
|
||||||
|
|
||||||
|
if dist_path.exists() {
|
||||||
|
std::fs::remove_dir_all(&dist_path).with_context(|| "Cannot delete `dist` directory")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir(&dist_path).with_context(|| "Cannot create `dist` directory")?;
|
||||||
|
|
||||||
|
for p in std::fs::read_dir(public_path)? {
|
||||||
|
copy_recursively(&p?.path(), &dist_path)
|
||||||
|
.with_context(|| "Cannot copy `public` -> `dist`")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{{ Generate contents
|
||||||
|
let djot_input = std::fs::read_to_string("content/arcaea.dj").unwrap();
|
||||||
|
let mut out = String::new();
|
||||||
|
let page_template = template!("templates/page.html")?;
|
||||||
|
let mut page_renderer = TemplateRenderer::new(&page_template);
|
||||||
|
|
||||||
|
while let Some(label) = page_renderer.next(&mut out)? {
|
||||||
|
if label == "content" {
|
||||||
|
let events = jotdown::Parser::new(&djot_input);
|
||||||
|
let html = Renderer::new()?;
|
||||||
|
html.push(events, &mut out)?;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page_renderer.finish(&mut out)?;
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
let posts_dir = dist_path.join("posts");
|
||||||
|
std::fs::create_dir(&posts_dir).with_context(|| "Cannot create `dist/posts` directory")?;
|
||||||
|
|
||||||
|
std::fs::write(posts_dir.join("arcaea.html"), out)
|
||||||
|
.with_context(|| "Failed to write `arcaea.html` post")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
157
src/template.rs
Normal file
157
src/template.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
// {{{ Templates & stops
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Stop {
|
||||||
|
label: String,
|
||||||
|
start: usize,
|
||||||
|
length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Template {
|
||||||
|
text: String,
|
||||||
|
stops: Vec<Stop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template {
|
||||||
|
// {{{ Parse template
|
||||||
|
#[allow(clippy::iter_nth_zero)]
|
||||||
|
pub fn parse(text: String) -> anyhow::Result<Template> {
|
||||||
|
let mut stops = Vec::new();
|
||||||
|
|
||||||
|
let open_stop = "{{";
|
||||||
|
let close_stop = "}}";
|
||||||
|
|
||||||
|
let mut current_stop: Option<Stop> = None;
|
||||||
|
let mut prev_ix = None;
|
||||||
|
for (ix, c) in text.char_indices() {
|
||||||
|
if let Some(prev) = prev_ix {
|
||||||
|
// This char, together with the previous one
|
||||||
|
let last_two = &text[prev..=ix];
|
||||||
|
if close_stop == last_two {
|
||||||
|
if let Some(mut stop) = current_stop.take() {
|
||||||
|
stop.label.pop().unwrap();
|
||||||
|
// I think this is safe, as } is ascii
|
||||||
|
stop.length = ix + 1 - stop.start;
|
||||||
|
stops.push(stop);
|
||||||
|
}
|
||||||
|
} else if open_stop == last_two && current_stop.is_none() {
|
||||||
|
current_stop = Some(Stop {
|
||||||
|
label: String::new(),
|
||||||
|
start: prev,
|
||||||
|
length: 0,
|
||||||
|
});
|
||||||
|
} else if let Some(stop) = current_stop.as_mut() {
|
||||||
|
stop.label.write_char(c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_ix = Some(ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Template { text, stops })
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Template rendering
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||||
|
enum RendererState {
|
||||||
|
Started,
|
||||||
|
InStop(usize),
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TemplateRenderer<'a> {
|
||||||
|
template: &'a Template,
|
||||||
|
state: RendererState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TemplateRenderer<'a> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(template: &'a Template) -> Self {
|
||||||
|
Self {
|
||||||
|
template,
|
||||||
|
state: RendererState::Started,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current placeholder label
|
||||||
|
pub fn current(&mut self, w: impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
|
||||||
|
let current_label = match self.state {
|
||||||
|
RendererState::Started => self.next(w)?,
|
||||||
|
RendererState::InStop(ix) => Some(self.template.stops[ix].label.as_str()),
|
||||||
|
RendererState::Finished => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(current_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to finish rendering.
|
||||||
|
pub fn finish(mut self, w: impl std::fmt::Write) -> anyhow::Result<()> {
|
||||||
|
if let Some(label) = self.next(w)? {
|
||||||
|
bail!("Attempting to finish template rendering before label `{label}` was handled");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{{ Advance to the next placeholder
|
||||||
|
/// Move onto the next placeholder
|
||||||
|
pub fn next(&mut self, mut w: impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
|
||||||
|
if self.state == RendererState::Finished {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, current_pos) = self.current_stop_range();
|
||||||
|
|
||||||
|
let next_stop_ix = match self.state {
|
||||||
|
RendererState::Started => 0,
|
||||||
|
RendererState::InStop(stop_ix) => stop_ix + 1,
|
||||||
|
RendererState::Finished => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = if next_stop_ix < self.template.stops.len() {
|
||||||
|
RendererState::InStop(next_stop_ix)
|
||||||
|
} else {
|
||||||
|
RendererState::Finished
|
||||||
|
};
|
||||||
|
|
||||||
|
let (next_pos, _) = self.current_stop_range();
|
||||||
|
w.write_str(&self.template.text[current_pos..next_pos])?;
|
||||||
|
|
||||||
|
let current_label = match self.state {
|
||||||
|
RendererState::InStop(ix) => Some(self.template.stops[ix].label.as_str()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(current_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_stop_range(&self) -> (usize, usize) {
|
||||||
|
match self.state {
|
||||||
|
RendererState::Started => (0, 0),
|
||||||
|
RendererState::InStop(stop_ix) => {
|
||||||
|
let stop = &self.template.stops[stop_ix];
|
||||||
|
(stop.start, stop.start + stop.length)
|
||||||
|
}
|
||||||
|
RendererState::Finished => (self.template.text.len(), self.template.text.len()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Macro
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! template {
|
||||||
|
($path:literal) => {{
|
||||||
|
static TEMPLATE_TEXT: &str = include_str!($path);
|
||||||
|
$crate::template::Template::parse(TEMPLATE_TEXT.to_owned())
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
// }}}
|
11
src/templates/aside.html
Normal file
11
src/templates/aside.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<details class="aside">
|
||||||
|
<summary class="aside-summary">
|
||||||
|
<img
|
||||||
|
class="aside-icon"
|
||||||
|
alt="{{character}}"
|
||||||
|
src="/assets/icons/{{character}}.webp"
|
||||||
|
/>
|
||||||
|
<span class="aside-title">{{title}}</span>
|
||||||
|
</summary>
|
||||||
|
<div class="aside-content">{{content}}</div>
|
||||||
|
</details>
|
|
@ -9,7 +9,7 @@
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<title>Moonythm</title>
|
<title>Moonythm</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
|
||||||
<!-- MathML -->
|
<!-- MathML -->
|
||||||
<link
|
<link
|
||||||
|
@ -25,6 +25,6 @@
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
$CONTENT
|
<div id="page-content">{{content}}</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue