Start working on angelfold february report
This commit is contained in:
parent
080559e7e0
commit
9938c0ee87
content/echoes
src
143
content/echoes/angelfold-2025-february/index.dj
Normal file
143
content/echoes/angelfold-2025-february/index.dj
Normal file
|
@ -0,0 +1,143 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
:::
|
||||
Rewriting a game from Rust to Odin, smooth screen transitions, plenty of UI rewrites, and planting the seeds of procedural animation.
|
||||
:::
|
||||
|
||||
# Angelfold progress report — February '25
|
||||
|
||||
Angelfold is a non-euclidean action/puzzle platformer I've been working on in my free time. For more details, check out the [main post][angelfold] about the game. I had been working on the game on and off for months, although a lot of it was (and still is!) exploratory work, or simply pondering the implications of the games' rules (ala [this talk][Jon Blow — truth in game design]).
|
||||
|
||||
[angelfold]: /echoes/angelfold
|
||||
[Jon Blow — truth in game design]: https://www.youtube.com/watch?v=C5FUtrmO7gI
|
||||
|
||||
::: toc
|
||||
:::
|
||||
|
||||
## The Rust → Odin rewrite
|
||||
|
||||
Most of my time last month has been taken by an effort to rewrite everything I had from Rust to Odin. While the number of lines of code is not a great metric for anything, this picture should give you an idea of the relative scale of the rewrite:
|
||||
|
||||
{ src="tokei-output.png" alt="7413 lines of rust code (out of which 5959 are actual code), split across 36 files" }
|
||||
::: image-figure
|
||||
Summary of the lines of count I had as part of the Rust version of Angelfold. Generated using [tokei][].
|
||||
|
||||
[tokei]: https://github.com/XAMPPRocky/tokei
|
||||
:::
|
||||
|
||||
### Why???
|
||||
|
||||
The main reason I went ahead with the rewrite is Rust's terrible compilation speeds, and the ecosystem's blunders when it comes to hot code reloading. When looking for advice on how to improve said compilation speeds, I came across two different styles of posts.
|
||||
|
||||
For exhibit A, we have discussions like [this one][rust forum compilation speed discussion], where the general consensus is that compilation speeds are either not an issue in the first place, that they shouldn't matter (as the only important bit is the speed at which [RLS][rust-language-server] (in modern terms, [rust analyzer][]) or [cargo check][] produce their output), or that (and this is a [quote][perhaps we should practice the discipline of the mathematician]) "perhaps we should all practice more of the discipline of the mathematician and practice less of the "hack it and see" style of the JavaScript programmer". In particular, the sentiment of spending hours without compiling the code and relying on compilation errors to guide correctness is something I've seen echoed all over Reddit and similar platforms.
|
||||
|
||||
Truth be told, I do think there's some truth to that! I am studying mathematics at uni, and there is a given discipline one forms when formalizing proofs (although that's not to say said discipline is perfect — that's why we have TAs checking students' proofs, or peer review for academic papers). Having a functional programming background, a side of me is sympathetic to the whole type driven development / "make impossible states unrepresentable" style of coding. At the same time, the truth is that a lot of coding just _isn't like that_.
|
||||
|
||||
For instance, tweaking gameplay-related values / logic is something that requires countless modifications to the code. A 2x speedup in compilation speeds might sound insignificant, but it makes the process so much nicer! The rust ecosystem does provide a way to tweak inline values via the [inline_tweak][] crate, although that does require the foresight of including the macro in the code in the first place. Still, I just wanted to be able to modify behaviour code and be able to test the results instantly — something Rust fought really hard to prevent me from doing.
|
||||
|
||||
[rust forum compilation speed discussion]: https://users.rust-lang.org/t/why-is-compilation-time-such-a-big-deal-for-you/41063/
|
||||
[rust-language-server]: https://github.com/rust-lang/rls
|
||||
[rust analyzer]: https://rust-analyzer.github.io/
|
||||
[cargo check]: https://doc.rust-lang.org/cargo/commands/cargo-check.html
|
||||
[perhaps we should practice the discipline of the mathematician]: https://users.rust-lang.org/t/why-is-compilation-time-such-a-big-deal-for-you/41063/18
|
||||
[inline_tweak]: https://docs.rs/inline_tweak/latest/inline_tweak/
|
||||
|
||||
The second kind of posts I found were those giving concrete tips on improving compilation speeds (here's a good [example][tips for faster rust compile times]). I followed as many tips as I could from articles of the like. I removed big dependencies like [`serde`][serde] in favour of smaller alternatives like [`nanoserde`][nanoserde], I moved to compiling using [`cranelift`][cranelift], I switched my linker to [`mold`][mold], removed lots of uses of procedural macros (in my own code, at least), enabled experimental parallel compilation, played with different optimization levels for dependencies/dependencies that include procedural macros, and more. If a tip is missing from this list, it's likely because I forgot to include it, not because I haven't tried it. In the end, I ended up with compilation speed of around `3s`. For context, I'm coding on a laptop with an `Intel i7-8565U` and 8GB of RAM — a bit dated, but it is what it is.
|
||||
|
||||
[tips for faster rust compile times]: https://corrode.dev/blog/tips-for-faster-rust-compile-times/
|
||||
[serde]: https://serde.rs/
|
||||
[nanoserde]: https://github.com/not-fl3/nanoserde
|
||||
[cranelift]: https://cranelift.dev/
|
||||
[mold]: https://github.com/rui314/mold
|
||||
|
||||
### Rust hates hot code reloading
|
||||
|
||||
Ok, the title might be a bit of a hyperbole, but hear me out. Even if compilation speeds were instant, having to recreate the same conditions to try something out after each tweak would be horrible. Hot code reloading allows swapping out most of the codebase for a newer version without throwing away the state. In Rust land, this is usually accomplished by compiling most of the codebase as a dynamic library, and having the static entrypoint swap it out between frames when changes are detected. An out of the box solution is provided by the [`hot_lib_reloader`][hot_lib_reloader] crate.
|
||||
|
||||
[hot_lib_reloader]: https://docs.rs/hot-lib-reloader/latest/hot_lib_reloader/
|
||||
|
||||
While the aforementioned crate works nicely in isolation, a lot of the ecosystem is... just not built with that in mind. The rust version of my codebase was using [`macroquad`][macroquad] for rendering (I'm planning to write my own once more of the gameplay is actually there), but macroquad holds internal state that simply goes away when swapping out the dynamic lib, oops! Were I using [`raylib`][raylib], I could simply enable the compilation flag that turns raylib into a dynamic library and have everything working (of course, all the changes I'm talking about here would be disabled in release mode)! Turning macroquad into a dynamic library is a very difficult task I'll talk about a bit later. The recommended solution (for example, the one described in the "If necessary, create indirections" section of [this article][hot reloading rust]) seems to be creating a layer of indirection, where the dynamic section of the codebase defines a `Renderer` trait, and then accepts a `Box<dyn Renderer>` from the static entrypoint in order to do all the rendering through. This way, all calls to the renderer are technically made by the static entrypoint, thus `macroquad` doesn't have to be reloaded each time.
|
||||
|
||||
:::: figure
|
||||
``` rust
|
||||
pub trait Renderer {
|
||||
fn alloc(&self) -> &bumpalo::Bump;
|
||||
...
|
||||
|
||||
fn is_key_down(&self, key: KeyCode) -> bool;
|
||||
fn mouse_position(&self) -> Vec2;
|
||||
...
|
||||
|
||||
fn camera_stack(&self) -> &CameraStack;
|
||||
fn push_camera(&mut self, transform: Affine2);
|
||||
...
|
||||
|
||||
fn clear_background(&mut self, color: Color);
|
||||
fn draw_circle(&mut self, center: Vec2, r: f32, color: Color);
|
||||
...
|
||||
|
||||
fn screen_dimensions(&self) -> Vec2;
|
||||
fn set_cursor(&mut self, cursor: CursorIcon);
|
||||
...
|
||||
}
|
||||
```
|
||||
::: caption
|
||||
Example `Renderer` trait for creating indirection when calling `macroquad`.
|
||||
:::
|
||||
::::
|
||||
|
||||
[macroquad]: https://macroquad.rs/
|
||||
[raylib]: https://www.raylib.com/
|
||||
[hot reloading rust]: https://robert.kra.hn/posts/hot-reloading-rust/
|
||||
|
||||
Notice the [`bumpalo`][bumpalo] usage in the code block above? `Bumpalo` is a crate that provides an arena allocator implementation. An arena allocator can be thought of as a preallocated block of memory, where allocations always happen by moving a pointer that tracks the amount of memory used so far forwards. At any point, we can trivially clear out the entire arena by simply moving the pointer back to the beginning of the region. This is super useful in games, where lots of state exists until the end of the current frame only. Moreover, bumping a pointer is much more efficient than asking the OS for more memory each time, while also simplifying memory management for the programmer (at least in theory — more on this later).
|
||||
|
||||
[bumpalo]: https://docs.rs/bumpalo/latest/bumpalo/
|
||||
|
||||
But guess what, it turns out `bumpalo` uses a static `EMPTY_CHUNK` pointer which it compares things to on cleanup, which means bad free errors start occurring the moment you try hot reloading it. I asked around in the official rust discord server, and people told me that is the intended behaviour, thus I didn't bother opening an issue (maybe I should've? Maybe the people in the discord were wrong?), and simply forked away the repo and fixed the issue myself. Having to fork libraries is a bit of a common theme with this project (I also had to fork `macroquad` for various issues, but those are less fundamental and thus beside the point).
|
||||
|
||||
While [`egui`][egui] is one of the prototypical [examples][egui hot-reloading example] for hot-reloading, I couldn't for the life of me get [`egui-miniquad`][egui-miniquad] to hot reload without errors, and thus gave up on trying to use it for UI.
|
||||
|
||||
[egui]: https://github.com/emilk/egui
|
||||
[egui hot-reloading example]: https://github.com/rksm/hot-lib-reloader-rs/tree/master/examples/hot-egui
|
||||
[egui-miniquad]: https://github.com/not-fl3/egui-miniquad
|
||||
|
||||
The obvious solution to all these issues is to try loading the various problematic libraries dynamically. Heck, the [aforementioned article][tips for faster rust compile times] even recommends doing it to improve compile times! Oh, sweet summer child... If only it was this easy. First of all, in rust land, the consumer of a library cannot decide to simply force the library into being dynamic — the library has to declare that by itself inside `Cargo.toml`. Thankfully, a tool exists to automate the creation of dynamic wrapper libraries around crates — see [this article][speeding up incremental rust compilation with dylibs] for details. In reality, I couldn't get that to work for any of my important dependencies.
|
||||
|
||||
The article does mention the [diamond dependency][] problem as a limitation of the tool, and for good reason — it is one hell of a limitation. The rust compiler does not allow a static library to be included more than once in the resulting artifacts, i.e. a dependency cannot be statically included both inside the main entrypoint _and_ inside one of its dynamically linked dependencies. This is... problematic to say the least. The rust ecosystem is filled with tiny NPM-style common dependencies that appear many levels down the dependency tree. The article recommends either forking all the libraries involved and making them point to a dynamic version of the common dependencies, or trying to PR the upstream library into enabling dynamic linking as a possibility for their library. Either of them is unfeasible when the issue occurs for a dozen of libraries, all many layers deep inside the dependency tree (that is, I would have to fork dozens of intermediate libraries, which is just not feasible...).
|
||||
|
||||
[speeding up incremental rust compilation with dylibs]: https://robert.kra.hn/posts/2022-09-09-speeding-up-incremental-rust-compilation-with-dylibs/
|
||||
[diamond dependency]: https://robert.kra.hn/posts/2022-09-09-speeding-up-incremental-rust-compilation-with-dylibs/#limitation-the-diamond-dependency-problem
|
||||
|
||||
If you do want to try hot reloading in rust, beware of the many footguns. For instance, my main binary depends on `macroquad`, while the dynamic library depends on [`glam`][glam] (a cool linear algebra library that `macroquad` depends on as well). "Everything sounds fine so far", I sense you thinking — WRONG. This will lead to horrible memory corruption and break all your rendering code. Why? Because `macroquad` disables the SIMD support in `glam` (in order to perform one less conversion when giving the data to the GPU, I've heard?), which in turn makes `glam` use a different memory layout for its vectors (since the memory alignment required for SIMD operations is no longer there). But guess what — since the dynamic library component of the codebase does not depend on `macroquad`, and SIMD is enabled by default in `glam`, the Rust compiler will simply compile `glam` with said feature flag active when compiling the dynamic library by itself (which is obviously something you'll be doing a lot of when hot code reloading), thus causing the two sides of the abyss that is dynamic linking to use different memory layouts...
|
||||
|
||||
[glam]: https://docs.rs/glam/latest/glam/
|
||||
|
||||
### You might not need the safety rust guarantees
|
||||
|
||||
Rust's safety guarantees are super nice, but over the course of the last month, I've kind of came to terms with the fact that... I don't really need them — not for this project at least. I'm sure memory management becomes a real problem in non-tiny teams, but it feels trivial when working on a project by myself. I haven't encountered a single use-after-free error throughout my time with Odin. Although I've used Rust for many projects before starting this game, and I feel like I've very much internalised the way Rust treats lifetimes, I still have moments when I'm refactoring and Rust starts yelling at me for doing things the wrong way, only for me to have to spend way too long thinking about the best way to make the compiler happy. Now, "that's a good thing — it means the compiler is preventing potential errors", I hear you say. I'm sure there's cases when this is the case, but for this game, that just hasn't been true. Heck, my experience refactoring Odin code has proven that memory management bugs are like... not that hard to avoid when working on a project by yourself.
|
||||
|
||||
The usual counter-argument to what I'm describing is that future-me is almost a different person than present-me, and that things will become a lot more dangerous when I'm revisiting pieces of code a few months into the future. I guess we'll have to revisit the issue in a few months, although I feel like the set of practices I've picked up along my Odin journey should help alleviate that a little, oh well...
|
||||
|
||||
The other counter-argument I heard brought up is that unsafe-rust is much safer than the language's alternatives, and that I can always just throw `unsafe` at the issue. I disagree. Unsafe rust is incredible unergonomic to work with. I have considered just always using `*mut` instead of `&mut` throughout my codebase, but things as simple as accessing a struct's property are not possible with `*mut` without casting into a `&mut` first, which is just incredibly verbose.
|
||||
|
||||
The other extreme I've heard people recommend is always wrapping things in `Arc<Mutex<_>>` or whatnot and calling it a day. While this does solve the issue to some extent, it's also incredibly unergonomic to work with, while also impacting (to a very very small extent) performance for no reason.
|
||||
|
||||
There's also patterns you just... cannot express (nicely) in Rust. For instance, I cannot have a struct which stores a custom allocator together with two vectors allocated with said allocator. Ok, it _is_ possible by instead storing a reference to said allocator, and taking in a lifetime parameter and... let's stop for a second. This is all incredibly annoying to do, and trivial in Odin. That's not to mention how the [`allocator_api`][allocator_api] seems to have [stalled][allocator_api has stalled] long ago (which is fine by the way — this is a volunteer run project), while custom allocators are integrated into the language out of the box in Odin.
|
||||
|
||||
[allocator_api]: https://doc.rust-lang.org/beta/unstable-book/library-features/allocator-api.html
|
||||
[allocator_api has stalled]: https://www.reddit.com/r/rust/comments/18yhmij/comment/kgb3jbt/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
|
||||
|
||||
### Why Odin
|
||||
|
||||
## Undo/redo
|
||||
|
||||
## The many UI rewrites
|
||||
|
||||
## Smooth screen transitions
|
||||
|
||||
## The entity system
|
||||
## Further work — the seeds of procedural animation
|
BIN
content/echoes/angelfold-2025-february/tokei-output.png
Normal file
BIN
content/echoes/angelfold-2025-february/tokei-output.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 50 KiB |
19
content/echoes/angelfold.dj
Normal file
19
content/echoes/angelfold.dj
Normal file
|
@ -0,0 +1,19 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
created_at = "2025-03-04T11:46:53+01:00"
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
:::
|
||||
Angelfold is a non-euclidean action/puzzle platformer I'm working on in my free time.
|
||||
:::
|
||||
|
||||
# Angelfold
|
||||
|
||||
Angelfold is a non-euclidean action/puzzle platformer I'm working on in my free time (read: god knows if this will ever get finished lol). This page is still under construction (I need to add more details and whatnot).
|
||||
|
||||
## Progress reports
|
||||
|
||||
I try to write articles going over the challenges and victories I encounter each month. Here you can find a complete list!
|
||||
|
||||
- [February '05](/echoes/angelfold-2025-february/)
|
|
@ -22,21 +22,21 @@ Our server features a bunch of tweaks to the base game:
|
|||
- _Hikari & Tairitsu (reunion)_ now defaults to dark mode (namely, the _Arcana Eden_ background) on charts without a predefined inverse (i.e. you can finally play _Fracture Ray_ in dark mode!). The only known chart where this doesn't work well is _Ether Strike_, where the field doesn't change colors for some bizarre reason.
|
||||
|
||||
{ src="darkswansong.png" alt="Swan song played in dark mode" }
|
||||
::: figure
|
||||
::: image-figure
|
||||
_Swan song_ being played in dark mode.
|
||||
:::
|
||||
|
||||
- When awakened, _Hikari & Tairitsu_'s world mode stats will be greatly improved. So is the case for _Lagrange (aria)_. Note that _Lagrange (aria)_ does not have an awakening in the base game, which means she'll be invisible when initially awakened (because of the lack of art). Use the button with two arrows as the icon to swap back to the original art. Together, these two partners can be used to blaze through any world-mode section no matter the preferred theme (i.e. light/dark) of play. We considered unlocking everything right away, but some people preferred to still have the ability to grind / experience the story as intended.
|
||||
|
||||
{ src="oplagrange.png" alt="Awakened Lagrange (aria) with her incredibly OP stats" }
|
||||
::: figure
|
||||
::: image-figure
|
||||
WYS- _gets shot_
|
||||
:::
|
||||
|
||||
- PTT is computed as `B30 + B10`, hence it can never go down. For many long-term Arcaea players, this is a dream come true. This does mean your PTT will likely be slightly higher than in the base game, as the lack of a recency element removes the variance. As an upside, this does mean you can now play silly stuff without the constant fear of losing PTT. This also makes the server a perfect way to practice hard stuff, or to let someone new to try the game without starting to worry the instant you realise you forgot to turn on airplane mode.
|
||||
|
||||
{ src="insanepttgain.png" alt="A screenshot of a single play gaining an insane amount of PTT, implying the account is still new" }
|
||||
::: figure
|
||||
::: image-figure
|
||||
Remember how easy gaining PTT was on a new account...
|
||||
:::
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
hidden = true
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
hidden = true
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
:::
|
||||
A subjective tier-list style ranking of the games I've played, together with extensive reasoning regarding my choices.
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
# Why I love Yu-Gi-Oh!
|
||||
|
||||
Lorem ipsum odor amet, consectetuer adipiscing elit. Per lacus at sociosqu curae varius nunc; magnis elit. Dictum dis tristique semper velit montes eleifend suscipit taciti. Himenaeos nunc morbi litora mi at molestie porttitor non sit. Convallis cursus ante tincidunt suspendisse class lobortis. Sodales fusce congue aliquet; eros lectus enim ullamcorper. Aptent fames laoreet odio pretium fermentum pharetra nisl fames sem.
|
||||
|
||||
Phasellus hendrerit eleifend nibh cubilia accumsan bibendum donec. Etiam torquent consequat hendrerit rhoncus fringilla elit sociosqu habitasse. Sagittis interdum magna bibendum consectetur ante. Vivamus non ligula fusce parturient mattis vulputate potenti. Eleifend aenean imperdiet augue, velit cras ornare nam. Sociosqu malesuada potenti mattis primis sapien condimentum tempus. Magnis pellentesque gravida conubia mattis adipiscing dis pretium facilisi orci. Et interdum sodales curae porta facilisis; vehicula curae euismod. Cubilia diam sociosqu neque aliquam, elementum ullamcorper per iaculis. Ut conubia duis maximus hac urna amet euismod.
|
||||
|
||||
Quisque rhoncus vitae scelerisque nostra sit eros est congue conubia. Ligula fusce vehicula rhoncus hac vel lacinia nisi porttitor. Gravida nisl neque rutrum efficitur sed leo ligula adipiscing. Eget et habitasse semper cursus, dictumst ex dis. Erat habitasse natoque adipiscing nulla lectus metus neque. Dolor vehicula consectetur aliquam quam sem cubilia purus elit. Himenaeos mauris vulputate facilisis at maecenas lacinia arcu nisl. Eu euismod ac metus suscipit ante. Interdum vitae per pellentesque rutrum finibus nisi feugiat. Duis dapibus amet condimentum; platea vestibulum laoreet.
|
||||
|
||||
Eu facilisi in risus nulla posuere ultrices curabitur. Pharetra curae euismod dapibus venenatis lectus netus metus. Conubia conubia arcu dui, tempus quis mattis. Inceptos magna pretium consequat eleifend condimentum nibh. Neque lacinia suscipit dapibus nunc consequat. Rutrum leo senectus purus primis donec ornare faucibus sem. Maximus vitae inceptos ligula; luctus fusce eu.
|
||||
|
||||
Ex metus dis est nisl; elementum fames cursus ligula platea. Quis porta enim conubia fermentum suspendisse a. Conubia eros fermentum vestibulum ultrices cubilia vestibulum. Himenaeos himenaeos conubia nunc curae netus nibh ante? Dui ex scelerisque praesent ultrices libero nunc lectus mattis. Arcu suspendisse dictum orci vivamus pretium taciti iaculis. Cras tincidunt ullamcorper litora lobortis hac cras maecenas ornare.
|
||||
|
42
src/html.rs
42
src/html.rs
|
@ -8,6 +8,7 @@ use anyhow::Context;
|
|||
use chrono::DateTime;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Utc;
|
||||
use jotdown::Alignment;
|
||||
use jotdown::AttributeValue;
|
||||
use jotdown::Container;
|
||||
|
@ -42,7 +43,7 @@ pub enum State<'s> {
|
|||
TextOnly,
|
||||
Ignore,
|
||||
Raw,
|
||||
Figure,
|
||||
ImageFigure,
|
||||
Strikethrough,
|
||||
Math(bool),
|
||||
CodeBlock(String),
|
||||
|
@ -276,7 +277,18 @@ impl<'s> Writer<'s> {
|
|||
// {{{ Post list
|
||||
Container::Div { class: "posts" } => {
|
||||
write!(out, r#"<ol class="article-list">"#)?;
|
||||
for post in self.pages {
|
||||
|
||||
let mut pages = self.pages.iter().collect::<Vec<_>>();
|
||||
pages.sort_by_key(|page| {
|
||||
if let Some(created_at) = page.config.created_at {
|
||||
(0, created_at)
|
||||
} else {
|
||||
(1, Utc::now().fixed_offset())
|
||||
}
|
||||
});
|
||||
pages.reverse();
|
||||
|
||||
for post in pages {
|
||||
// Skip non-posts
|
||||
if !matches!(post.route, PageRoute::Post(_)) {
|
||||
continue;
|
||||
|
@ -414,7 +426,17 @@ impl<'s> Writer<'s> {
|
|||
// }}}
|
||||
// {{{ Figure
|
||||
Container::Div { class: "figure" } => {
|
||||
self.states.push(State::Figure);
|
||||
out.write_str("<figure>")?;
|
||||
}
|
||||
Container::Div { class: "caption" } => {
|
||||
out.write_str("<figcaption>")?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Image figure
|
||||
Container::Div {
|
||||
class: "image-figure",
|
||||
} => {
|
||||
self.states.push(State::ImageFigure);
|
||||
let alt = attrs.get_value("alt").ok_or_else(|| {
|
||||
anyhow!("Figure element encountered without an `alt` attribute")
|
||||
})?;
|
||||
|
@ -559,8 +581,10 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Figure
|
||||
Container::Div { class: "figure" } => {
|
||||
let State::Figure = self.states.pop().unwrap() else {
|
||||
Container::Div {
|
||||
class: "image-figure",
|
||||
} => {
|
||||
let State::ImageFigure = self.states.pop().unwrap() else {
|
||||
panic!(
|
||||
"Arrived at end of figure without being in the approriate state."
|
||||
);
|
||||
|
@ -569,6 +593,14 @@ impl<'s> Writer<'s> {
|
|||
write!(out, "</figcaption></figure>")?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Figure
|
||||
Container::Div { class: "figure" } => {
|
||||
out.write_str("</figure>")?;
|
||||
}
|
||||
Container::Div { class: "caption" } => {
|
||||
out.write_str("</figcaption>")?;
|
||||
}
|
||||
// }}}
|
||||
Container::Heading { level, .. } => {
|
||||
write!(out, "</h{}>", level)?;
|
||||
|
||||
|
|
Loading…
Reference in a new issue