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
|
||||
|
||||
## 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
|
||||
|
||||
$$`\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"}
|
||||
:::: in-depth
|
||||
::: in-depth-header
|
||||
{.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.
|
||||
{title="Lagrange's extras: ζ-scoring" character="lagrange"}
|
||||
::: aside
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
|
@ -2,67 +2,75 @@ html {
|
|||
font: 100%/1.5 sans-serif;
|
||||
}
|
||||
|
||||
#page-content {
|
||||
max-width: 70ch;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1.25rem;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.in-depth {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.in-depth-header {
|
||||
/* {{{ Asides*/
|
||||
.aside {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.aside-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.in-depth-header > .in-depth-heading {
|
||||
.aside-summary > .aside-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.in-depth-header > * {
|
||||
.aside-summary > * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img.in-depth-icon {
|
||||
img.aside-icon {
|
||||
height: 1.75rem;
|
||||
margin-right: 0.5rem;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
details {
|
||||
.aside {
|
||||
background: #ead3ed;
|
||||
border-radius: 3px;
|
||||
padding: 0.5rem 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
summary {
|
||||
.aside-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
.aside-summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
summary:before {
|
||||
.aside-summary:before {
|
||||
content: "▶";
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.75rem 0 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
details[open] summary:before {
|
||||
.aside[open] .aside_summary:before {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
details > .in-depth {
|
||||
.aside > .aside-content {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* }}}*/
|
||||
|
|
313
src/html.rs
313
src/html.rs
|
@ -2,82 +2,99 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use jotdown::Alignment;
|
||||
use jotdown::Container;
|
||||
use jotdown::Event;
|
||||
use jotdown::LinkType;
|
||||
use jotdown::ListKind;
|
||||
use jotdown::OrderedListNumbering::*;
|
||||
use jotdown::Render;
|
||||
use jotdown::RenderRef;
|
||||
use jotdown::SpanLinkType;
|
||||
|
||||
use crate::template;
|
||||
use crate::template::Template;
|
||||
use crate::template::TemplateRenderer;
|
||||
|
||||
// {{{ Renderer
|
||||
/// 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
|
||||
I: Iterator<Item = Event<'s>>,
|
||||
{
|
||||
let mut s = String::new();
|
||||
Renderer::default().push(events, &mut s).unwrap();
|
||||
s
|
||||
Renderer::new()?.push(events, &mut s)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// [`Render`] implementor that writes HTML output.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Renderer {}
|
||||
|
||||
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)
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Renderer {
|
||||
templates: BuiltinTempaltes,
|
||||
}
|
||||
|
||||
impl RenderRef for Renderer {
|
||||
fn push_ref<'s, E, I, W>(&self, mut events: I, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
E: AsRef<Event<'s>>,
|
||||
I: Iterator<Item = E>,
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
let mut w = Writer::new();
|
||||
events.try_for_each(|e| w.render_event(e.as_ref(), &mut out))?;
|
||||
w.render_epilogue(&mut out)
|
||||
impl Renderer {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
templates: BuiltinTempaltes::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push<'s>(
|
||||
&self,
|
||||
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> {
|
||||
list_tightness: Vec<bool>,
|
||||
states: Vec<State>,
|
||||
states: Vec<State<'s>>,
|
||||
footnotes: Footnotes<'s>,
|
||||
renderer: &'s Renderer,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum State {
|
||||
#[derive(Debug, Clone)]
|
||||
enum State<'s> {
|
||||
TextOnly,
|
||||
Ignore,
|
||||
Raw,
|
||||
Math(bool),
|
||||
Aside(TemplateRenderer<'s>),
|
||||
}
|
||||
|
||||
impl<'s> Writer<'s> {
|
||||
fn new() -> Self {
|
||||
fn new(renderer: &'s Renderer) -> Self {
|
||||
Self {
|
||||
list_tightness: Vec::new(),
|
||||
states: Vec::new(),
|
||||
footnotes: Footnotes::default(),
|
||||
renderer,
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
|
@ -101,7 +118,7 @@ impl<'s> Writer<'s> {
|
|||
return Ok(());
|
||||
}
|
||||
Event::End(Container::LinkDefinition { .. }) => {
|
||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
||||
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||||
self.states.pop();
|
||||
}
|
||||
|
||||
|
@ -116,9 +133,9 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
Event::End(Container::RawBlock { format } | Container::RawInline { format }) => {
|
||||
if format == &"html" {
|
||||
assert_eq!(self.states.last(), Some(&State::Raw));
|
||||
assert!(matches!(self.states.last(), Some(State::Raw)));
|
||||
} else {
|
||||
assert_eq!(self.states.last(), Some(&State::Ignore));
|
||||
assert!(matches!(self.states.last(), Some(State::Ignore)));
|
||||
};
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
match e {
|
||||
// {{{ Container start
|
||||
Event::Start(c, attrs) => {
|
||||
if self.states.last() == Some(&State::TextOnly) {
|
||||
if matches!(self.states.last(), Some(&State::TextOnly)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -200,6 +217,35 @@ impl<'s> Writer<'s> {
|
|||
Container::Table => out.write_str("<table")?,
|
||||
Container::TableRow { .. } => out.write_str("<tr")?,
|
||||
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::Heading { level, .. } => write!(out, "<h{}", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("<td")?,
|
||||
|
@ -220,85 +266,93 @@ impl<'s> Writer<'s> {
|
|||
Container::LinkDefinition { .. } => return Ok(()),
|
||||
}
|
||||
|
||||
// {{{ Write attributes
|
||||
let mut id_written = false;
|
||||
let mut class_written = false;
|
||||
let mut write_attribs = true;
|
||||
if matches!(c, Container::Div { class: "aside" }) {
|
||||
write_attribs = false;
|
||||
}
|
||||
|
||||
for (a, v) in attrs.unique_pairs() {
|
||||
write!(out, r#" {}=""#, a)?;
|
||||
v.parts().try_for_each(|part| write_attr(part, &mut out))?;
|
||||
match a {
|
||||
"class" => {
|
||||
class_written = true;
|
||||
write_class(c, true, &mut out)?;
|
||||
if write_attribs {
|
||||
// {{{ Write attributes
|
||||
let mut id_written = false;
|
||||
let mut class_written = false;
|
||||
|
||||
if write_attribs {
|
||||
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 {
|
||||
id,
|
||||
has_section: false,
|
||||
..
|
||||
}
|
||||
| Container::Section { id } = &c
|
||||
{
|
||||
if !id_written {
|
||||
out.write_str(r#" id=""#)?;
|
||||
write_attr(id, &mut out)?;
|
||||
// }}}
|
||||
// {{{ Write default ids/classes
|
||||
if let Container::Heading {
|
||||
id,
|
||||
has_section: false,
|
||||
..
|
||||
}
|
||||
| Container::Section { id } = &c
|
||||
{
|
||||
if !id_written {
|
||||
out.write_str(r#" id=""#)?;
|
||||
write_attr(id, &mut out)?;
|
||||
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('"')?;
|
||||
}
|
||||
// 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 {
|
||||
// {{{ Write css for aligning table cell text
|
||||
Container::TableCell { alignment, .. }
|
||||
if !matches!(alignment, Alignment::Unspecified) =>
|
||||
{
|
||||
let a = match alignment {
|
||||
Alignment::Unspecified => unreachable!(),
|
||||
Alignment::Left => "left",
|
||||
Alignment::Center => "center",
|
||||
Alignment::Right => "right",
|
||||
};
|
||||
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#"">"#)?;
|
||||
match c {
|
||||
// {{{ Write css for aligning table cell text
|
||||
Container::TableCell { alignment, .. }
|
||||
if !matches!(alignment, Alignment::Unspecified) =>
|
||||
{
|
||||
let a = match alignment {
|
||||
Alignment::Unspecified => unreachable!(),
|
||||
Alignment::Left => "left",
|
||||
Alignment::Center => "center",
|
||||
Alignment::Right => "right",
|
||||
};
|
||||
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#"">"#)?;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
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 {
|
||||
|
@ -313,13 +367,13 @@ impl<'s> Writer<'s> {
|
|||
Event::End(c) => {
|
||||
match &c {
|
||||
Container::Image(..) => {
|
||||
assert_eq!(self.states.last(), Some(&State::TextOnly));
|
||||
assert!(matches!(self.states.last(), Some(State::TextOnly)));
|
||||
self.states.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.states.last() == Some(&State::TextOnly) {
|
||||
if matches!(self.states.last(), Some(State::TextOnly)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -374,6 +428,15 @@ impl<'s> Writer<'s> {
|
|||
Container::Table => out.write_str("</table>")?,
|
||||
Container::TableRow { .. } => out.write_str("</tr>")?,
|
||||
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::Heading { level, .. } => write!(out, "</h{}>", level)?,
|
||||
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
|
||||
|
@ -428,7 +491,7 @@ impl<'s> Writer<'s> {
|
|||
// {{{ Footnote reference
|
||||
Event::FootnoteReference(label) => {
|
||||
let number = self.footnotes.reference(label);
|
||||
if self.states.last() != Some(&State::TextOnly) {
|
||||
if !matches!(self.states.last(), Some(State::TextOnly)) {
|
||||
write!(
|
||||
out,
|
||||
r##"<a id="fnref{}" href="#fn{}" role="doc-noteref"><sup>{}</sup></a>"##,
|
||||
|
@ -468,10 +531,7 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
|
||||
// {{{ Render epilogue
|
||||
fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
fn render_epilogue(&mut self, mut out: impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
if self.footnotes.reference_encountered() {
|
||||
out.write_str("<section role=\"doc-endnotes\">")?;
|
||||
out.write_str("<hr>")?;
|
||||
|
@ -545,24 +605,21 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_text<W>(s: &str, out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
#[inline]
|
||||
fn write_text(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||||
write_escape(s, false, out)
|
||||
}
|
||||
|
||||
fn write_attr<W>(s: &str, out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
#[inline]
|
||||
fn write_attr(s: &str, out: impl std::fmt::Write) -> std::fmt::Result {
|
||||
write_escape(s, true, out)
|
||||
}
|
||||
|
||||
fn write_escape<W>(mut s: &str, escape_quotes: bool, mut out: W) -> std::fmt::Result
|
||||
where
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
fn write_escape(
|
||||
mut s: &str,
|
||||
escape_quotes: bool,
|
||||
mut out: impl std::fmt::Write,
|
||||
) -> std::fmt::Result {
|
||||
let mut ent = "";
|
||||
while let Some(i) = s.find(|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 template;
|
||||
mod tex;
|
||||
fn main() {
|
||||
let djot_input = std::fs::read_to_string("content/arcaea.dj").unwrap();
|
||||
let events = jotdown::Parser::new(&djot_input);
|
||||
let html = crate::html::render_to_string(events);
|
||||
println!("{html}");
|
||||
|
||||
fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||
Command::new("cp").arg("-r").arg(from).arg(to).output()?;
|
||||
|
||||
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" />
|
||||
<title>Moonythm</title>
|
||||
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
|
||||
<!-- MathML -->
|
||||
<link
|
||||
|
@ -25,6 +25,6 @@
|
|||
/>
|
||||
</head>
|
||||
<body>
|
||||
$CONTENT
|
||||
<div id="page-content">{{content}}</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue