diff --git a/content/arcaea.dj b/content/arcaea.dj index 82686d0..eabee96 100644 --- a/content/arcaea.dj +++ b/content/arcaea.dj @@ -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 diff --git a/public/styles.css b/public/styles.css index d809e7f..1280f81 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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; } +/* }}}*/ diff --git a/src/html.rs b/src/html.rs index 0a8a569..42df1b5 100644 --- a/src/html.rs +++ b/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 { diff --git a/src/main.rs b/src/main.rs index bed9ece..486632f 100644 --- a/src/main.rs +++ b/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(()) } diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..a206ad4 --- /dev/null +++ b/src/template.rs @@ -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()) + }}; +} +// }}} diff --git a/src/templates/aside.html b/src/templates/aside.html new file mode 100644 index 0000000..4a3113a --- /dev/null +++ b/src/templates/aside.html @@ -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> diff --git a/public/index.html b/src/templates/page.html similarity index 86% rename from public/index.html rename to src/templates/page.html index 3bb6d7a..d9d912c 100644 --- a/public/index.html +++ b/src/templates/page.html @@ -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>