Allow building without .git
This commit is contained in:
parent
942c0fc9d4
commit
cda7f88a33
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -61,9 +61,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.32"
|
||||
version = "1.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "538b056773ee67775e422d15c33169f5415706855814b96505c84ee942f2bae2"
|
||||
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
@ -232,7 +232,9 @@ dependencies = [
|
|||
"sha2",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"tree-sitter-djot",
|
||||
"tree-sitter-highlight",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-rust",
|
||||
]
|
||||
|
||||
|
@ -437,6 +439,15 @@ dependencies = [
|
|||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-djot"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/treeman/tree-sitter-djot?rev=eb31845d59b9ee8c1b2098e78e9ca72004bd1579#eb31845d59b9ee8c1b2098e78e9ca72004bd1579"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-highlight"
|
||||
version = "0.24.3"
|
||||
|
@ -450,6 +461,16 @@ dependencies = [
|
|||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-html"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language"
|
||||
version = "0.1.2"
|
||||
|
|
|
@ -17,5 +17,7 @@ chrono = { version = "0.4.38", features = ["serde"] }
|
|||
tree-sitter = "0.24.3"
|
||||
tree-sitter-rust = "0.23.0"
|
||||
tree-sitter-highlight = "0.24.3"
|
||||
tree-sitter-djot = { git="https://github.com/treeman/tree-sitter-djot", rev="eb31845d59b9ee8c1b2098e78e9ca72004bd1579" }
|
||||
tree-sitter-html = "0.23.2"
|
||||
sha2 = "0.10.8"
|
||||
base64 = "0.22.1"
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
sitemap_exclude = true
|
||||
```
|
||||
|
||||
# Ooooops
|
||||
# sdlkfjsldkfjsldjkfslfj
|
||||
|
||||
This page does not seem to exist. If this page was here before, please [contact me][Page containing my contact info].
|
||||
Your spell slowly fizzles, for the wisdom you're seeking does not dwell here. Had this not always been this way, please [contact me][Page containing my contact info].
|
||||
|
||||
[Page containing my contact info]: /
|
||||
|
|
BIN
content/echoes/arcaea-pookies/darkswansong.png
Normal file
BIN
content/echoes/arcaea-pookies/darkswansong.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 949 KiB |
75
content/echoes/arcaea-pookies/index.dj
Normal file
75
content/echoes/arcaea-pookies/index.dj
Normal file
|
@ -0,0 +1,75 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
hidden = true
|
||||
```
|
||||
|
||||
# Arcaea for pookies
|
||||
|
||||
This is a private server for the rhythm game Arcaea. If you found this page, then feel free to join! Report any bugs to me via your preferred channel of communication.
|
||||
|
||||
## Features
|
||||
|
||||
Our server features a bunch of tweaks to the base game:
|
||||
|
||||
- Remove mobile-gamey elements: world mode stamina now recharges instantly, and every player is given an insurmountable amount of memories.
|
||||
- The `lookstothemoon` code is a sort of starter pack that can be redeemed to get a ton of fragments, ether drops, _Hikari & Tairitsu (reunion)_, and _Lagrange (aria)_
|
||||
- _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
|
||||
_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
|
||||
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
|
||||
Remember how easy gaining PTT was on a new account...
|
||||
:::
|
||||
|
||||
- The `blahajs_gift` code can be redeemed to gain access to all the partners that are normally only obtainable by buying the physical releases of the game's music. We are still thinking about the best way to handle event partners.
|
||||
|
||||
|
||||
## The team
|
||||
|
||||
Running this server wouldn't have been possible without the awesome server implementation maintained by [rlarhsid](https://github.com/rlarhsid) and [ariidesu](https://github.com/ariidesu) — a fork of the legendary server implementation by [Lost](https://github.com/Lost-MSth). The server runs a bunch of patches written by me. I also wrote nix derivations for both the server and other Arcaea-related tools, and wrapped everything neatly into a nixos module running on my server.
|
||||
|
||||
A lot of help and support has been put in by `@_.mxi0._`, who also performed all the API encryption (using arii's tool) and app patching. Him and I are the two admins currently managing the server.
|
||||
|
||||
I would also like to thank:
|
||||
|
||||
- `@dyuan01` and `@tayalex.2196` for the many bug reports
|
||||
- `@_.4nt1ph0n._` for helping me figure out a complete list of the physical music release and event partners
|
||||
- `@siloricity` for helping with chart bundle info on a certain deleted chart (which we intend to add back to the game at some point...)
|
||||
|
||||
## Installation
|
||||
|
||||
On Android, all you have to do is install the latest modded APK. Version `6.2.3` can be downloaded from either [Mega](https://mega.nz/file/kqJk1ZiY#2_D1pOpC1LgXzabxn6SxcWYHVc0-eDf4M-sYi7feM6s) or [Google Drive](https://drive.google.com/file/d/1azT2r70KCGh8U0K1W31h8AaD0Ykj68mh/view?usp=drive_link).
|
||||
|
||||
|
||||
{ title="Account creation" character="lagrange" id="account-creation" }
|
||||
::: char-aside
|
||||
Feel free to use a fake email when creating an account on our server! (we don't use it for anything). Make sure to remember your password though, as there's currently no automatic way to reset it (i.e. you'll have to message me to do it).
|
||||
:::
|
||||
|
||||
### IOS
|
||||
|
||||
On IOS, installation is much trickier, and will require an external computer:
|
||||
|
||||
1. Download the latest patched IPA. Version `6.2.0` can be downloaded from [Google Drive](https://drive.google.com/file/d/1yUJvieapV48S9RjpPeUBcxhUgORILDSy/view?usp=drive_link).
|
||||
2. Install [Sideloadly](https://sideloadly.io/) on your computer.
|
||||
3. Install the web version of _iTunes_ on your computer (*do not install the Microsoft store version*). You can find links to this on the _Sideloadly_ page. The _Sideloadly_ page also mentions needing _iCloud_, although I think I managed to perform the installation without it.
|
||||
4. Connect your apple device via cable to the computer. You can also enable WiFi transfers by following the guide on the _Sideloadly_ page.
|
||||
5. Open _Sideloadly_, and load the IPA file you downloaded earlier.
|
||||
6. Make sure the refresh option on the left of the "start" button is enabled.
|
||||
7. Go to advanced options, and override the app version to match the latest Android version of the patched app (in this case, `6.2.3`). You can also change the name of the app, if you so desire.
|
||||
8. Log into _Sideloadly_ with your Apple ID, and start the sideloading process. Along the way, you'll likely have to enable "developer mode" on your apple device (the steps required differ from IOS version to version), and trust apps sideloaded by the apple ID transferring the app (in this case, your ID).
|
||||
|
||||
You will need to refresh the installation every week. This should happen automatically when you connect your device to your computer by cable around the expiration date. This can also be performed via WiFi by following the steps outlined in the _Sideloadly_ documentation. For troubleshooting and help, contact me.
|
BIN
content/echoes/arcaea-pookies/insanepttgain.png
Normal file
BIN
content/echoes/arcaea-pookies/insanepttgain.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 MiB |
BIN
content/echoes/arcaea-pookies/oplagrange.png
Normal file
BIN
content/echoes/arcaea-pookies/oplagrange.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
|
@ -1,6 +1,5 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
created_at = "2024-11-02T05:13:44+01:00"
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{ role=description }
|
||||
:::
|
||||
A subjective tier-list style ranking of the games I've played, together with extensive reasoning regarding my choices.
|
||||
:::
|
||||
|
||||
# My thoughts on games
|
||||
|
||||
This article contains a highly subjective tier list of most of the games I've played. You can click on the different tiers or games to be taken to the section of the article exploring my thoughts on said game. Everything in this article is spoiler-free, unless otherwise stated in a certain game's section. Enjoy!
|
||||
|
@ -192,9 +197,9 @@ This tier contains games that are truly special, although haven't implanted them
|
|||
|
||||
Ultrakill is a game all about doing cool shit. There's so many instances where you think "wait, wouldn't it be awesome if I could ...", you try it, only for it to exceed your expectations. I don't play first person shooters, yet I've had a very good time ultrakilling demons.
|
||||
|
||||
After each level, the game rates you on three axes: kills, time, and style. Improving until you can chain together stylish combos in order to P-rank (i.e. perfect) a hard level is super fun. Ultrakill has some of my favourite boss fights in all of gaming, all to the tune of a soundtrack that goes incredibly hard.
|
||||
After each level, the game rates you on three axis: kills, time, and style. Improving until you can chain together stylish combos in order to P-rank (i.e. perfect) a hard level is super fun. Ultrakill has some of my favourite boss fights in all of gaming, all to the tune of a soundtrack that goes incredibly hard.
|
||||
|
||||
The game has some technical issues (it often forgets the resolution you configure in the settings after alt-tabbing around, but this seems to be an issue a lot of Unity games have, so I'll add it to the ever-growing list of justicications for me hating engines). The community seems to be a bit predisposed to spoiling people on gameplay elements, so if you want to go in blind, I recommend staying away from any kind of content related to the game.^
|
||||
The game has some technical issues (it often forgets the resolution you configure in the settings after alt-tabbing around, but this seems to be an issue a lot of Unity games have, so I'll add it to the ever-growing list of justicications for me hating engines). The community seems to be a bit predisposed to spoiling people on gameplay elements, so if you want to go in blind, I recommend staying away from any kind of content related to the game.
|
||||
|
||||
{ #amazing }
|
||||
## Amazing
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
{ role="config" }
|
||||
``` =toml
|
||||
created_at = "2025-03-03T18:42:56+01:00"
|
||||
```
|
||||
|
||||
{ role=description }
|
||||
:::
|
||||
An overview of the inner workings and technical decisions behind this website, including my reasons for choosing [djot](https://djot.net/) over markdown, rendering LaTeX quickly, templating without needless allocations, and more.
|
||||
|
@ -5,16 +10,161 @@ An overview of the inner workings and technical decisions behind this website, i
|
|||
|
||||
# The realm's secrets
|
||||
|
||||
## Djot (why not markdown?)
|
||||
- extensionability
|
||||
- writing my own html generator
|
||||
- templating
|
||||
- metadata
|
||||
This website is built on top of my own Rust-based static site generation scripts. I could've simply used an off-the-shelf static site generator (think [Hugo][]), but where would the fun in that be? (alskdjfslkdjflskdfj, I need help, I wasted way too much time on this, aaaaaaa). Ok, here we go — this article will go over the different decisions behind this website.
|
||||
|
||||
## Hosting
|
||||
- hosted on my nixos server
|
||||
- there's interesting stuff happening there too, but that deserves it's own post
|
||||
[Hugo]: https://gohugo.io/
|
||||
|
||||
::: toc
|
||||
:::
|
||||
|
||||
## Djot
|
||||
|
||||
This content for this website is written in [Djot][]! The main reason behind this choice was a desire for extensibility. While there are a million and one custom templating / component formats built on top of [Markdown][], I wanted to build the website on top of a format made with extensibility in mind. Enter Djot — a new format cooked up by the creator of [Pandoc][] and [Commonmark][]. The main feature that caught my attention was the ability to attach arbitrary classes/ids to blocks/inline sections (think divs and spans respectively). My code generator is then able to use said classes/ids to guide custom behaviour.
|
||||
|
||||
[Djot]: https://djot.net/
|
||||
[Markdown]: https://en.wikipedia.org/wiki/Markdown
|
||||
[Pandoc]: https://pandoc.org/
|
||||
[Commonmark]: https://commonmark.org/
|
||||
|
||||
For instance, I can create character asides like this:
|
||||
|
||||
```djot
|
||||
{ title="Character aside example" character="lagrange" id="highlighting-test" }
|
||||
::: char-aside
|
||||
Meow
|
||||
:::
|
||||
```
|
||||
|
||||
{ title="Character aside example" character="lagrange" id="highlighting-test" }
|
||||
::: char-aside
|
||||
Meow
|
||||
:::
|
||||
|
||||
### Generating HTML
|
||||
|
||||
I used the existing html generator from the [jotdown][] as a starting point, but made heavy modifications to the code in order to support the features my heart longed for (like the example above!).
|
||||
|
||||
[jotdown]: https://docs.rs/jotdown/latest/jotdown/
|
||||
|
||||
Although repeated [`write!`][write!] calls work well enough for generating simple HTML, I felt the need for a more robust templating system for creating more complex elements. The system I ended up with works pretty well, although a bit simplistic feature-wise (which is by design, _copium_). For instance, the aforementioned character asides are powered by this template:
|
||||
|
||||
```html
|
||||
<aside class="aside" aria-labelledby="{{id}}">
|
||||
<div class="aside-header">
|
||||
<img
|
||||
alt="{{character}}"
|
||||
src="/assets/icons/characters/{{character}}.webp"
|
||||
/>
|
||||
<h3 id="{{id}}">{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{content}}
|
||||
</aside>
|
||||
```
|
||||
|
||||
The template text gets embedded into the final binary (using [`incude_str!`][include\_str!]), then gets parsed at most once (using an [`OnceCell`][OnceCell]), into what essentially boils down into a list of ranges where content should be inserted:
|
||||
|
||||
```rust
|
||||
struct Stop<'s> {
|
||||
label: &'s str,
|
||||
start: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
pub struct Template<'s> {
|
||||
text: &'s str,
|
||||
stops: Vec<Stop<'s>>,
|
||||
}
|
||||
```
|
||||
|
||||
Although there's a few different ways I can use such a template, the most convenient one looks something like this:
|
||||
|
||||
```rust
|
||||
template!("templates/table-of-contents.html", out)?.feed(
|
||||
out,
|
||||
|label, out| {
|
||||
if label == "content" {
|
||||
write!(...);
|
||||
Ok(true) // Keep iterating!
|
||||
} else {
|
||||
Ok(false) // We're done (for now)
|
||||
}
|
||||
},
|
||||
)?;
|
||||
```
|
||||
|
||||
Since a `TemplateRenderer` is just a struct (that doesn't allocate at all, mind you), I'm free to keep partially filled templates around until more djot parsing events get processed, without having to keep the partially-filled inputs in memory.
|
||||
|
||||
This theme of minimizing allocations when possible is something I've been trying to take to heart (even more in the period since). Both djot and LaTeX parsing is done using pulldown parsers (via [jotdown][] and [pulldown_latex][] respectively). That is, my code generator consumes streams of events instead of constructing an in-memory AST.
|
||||
|
||||
[write!]: https://doc.rust-lang.org/std/macro.write.html
|
||||
[include\_str!]: https://doc.rust-lang.org/std/macro.include_str.html
|
||||
[OnceCell]: https://doc.rust-lang.org/std/cell/struct.OnceCell.html
|
||||
[pulldown_latex]: https://github.com/carloskiki/pulldown-latex
|
||||
|
||||
### LaTeX
|
||||
|
||||
Most websites I've come across seem to use either [MathJax][] or [KaTeX][] for LaTeX rendering. Shipping JavaScript would not be acceptable for this website, therefore MathJax was out of the equation (since as far as I understand, it operates on the client side only). I think KaTeX _can_ be used to pre-render LaTeX blocks on the server (although the the sites I've seen using it seemed not to do that for some reason). I don't particularly remember why I didn't go with it, although I guess the fact it runs on NodeJS would make usage from rust a bit painful. Oh well, I have working math, so that's what matters!
|
||||
|
||||
{ title="The ever elusive spider" character="lagrange" id="the-ever-elusive-spider" }
|
||||
::: char-aside
|
||||
$`\ddot \ddot \ddot \ddot \omega`
|
||||
:::
|
||||
|
||||
[MathJax]: https://www.mathjax.org/
|
||||
[KaTeX]: https://katex.org/
|
||||
|
||||
### Gemini
|
||||
|
||||
Another cool benefit of using Djot is being able to define links separately from their usage:
|
||||
|
||||
```djot
|
||||
Hello there, check out [my link][cool-link]!
|
||||
|
||||
[cool-link]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
```
|
||||
|
||||
While cool in and of itself, this means I should (in the future) have an easier time generating gemtext files for the [gemini protocol][gemini]. I won't go into detail here, as I don't currently have such a generator working, but gemtext doesn't support inline links (only on standalone lines), therefore link definition can be reused to have full control over the placement of such links!
|
||||
|
||||
[gemini]: https://en.wikipedia.org/wiki/Gemini_(protocol)
|
||||
|
||||
### Metadata
|
||||
|
||||
I don't think there's any consensus (nor any built-in way) on how to include document metadata in Djot documents. I do it through a combination of blocks:
|
||||
|
||||
- The description of each article gets read from `description` blocks. Said description can later be displayed in places like pages listing various articles. Here's how it looks:
|
||||
|
||||
```djot
|
||||
{ role=description }
|
||||
:::
|
||||
According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground.
|
||||
:::
|
||||
```
|
||||
|
||||
- Other metadata can be included through `config` blocks. There's no required fields as of now, and some fields are implied by other fields (i.e. `hidden` implies `sitemap_exclude`). Moreover, not all metadata is stored in the file itself. Properties like the `last_changed` date are taken directly from git!
|
||||
|
||||
````djot
|
||||
{ role=config }
|
||||
``` =toml
|
||||
hidden = ...
|
||||
created_at = ...
|
||||
|
||||
sitemap_exclude = ...
|
||||
sitemap_priority = ...
|
||||
sitemap_changefreq = ...
|
||||
```
|
||||
````
|
||||
|
||||
## Further work
|
||||
|
||||
There's quite a few things this website is missing. Here's a non-exhausting list:
|
||||
|
||||
- [rss][]/[atom][] feeds
|
||||
- [webmention][] support
|
||||
- [gemini][] version of the site
|
||||
- a dark theme
|
||||
|
||||
[rss]: https://en.wikipedia.org/wiki/RSS
|
||||
[atom]: https://en.wikipedia.org/wiki/Atom_(web_standard)
|
||||
[webmention]: https://indieweb.org/Webmention
|
||||
|
||||
## LaTeX
|
||||
- tried using `cached` with `latexmlmath`, but results were slow.
|
||||
- ended up using the `pulldown-latex` crate.
|
||||
|
|
|
@ -4,7 +4,7 @@ Welcome to my ethereal realm (read: website). This place is still being conjured
|
|||
|
||||
I'm known as `prescientmoon`, or sometimes `PGW`. I study mathematics, but I also really like coding and all computer-related sorcery. I love games of all kinds, although rhythm games have a special place in my heart.
|
||||
|
||||
You can message me in the following places \^-\^
|
||||
You can message me in the following places `:3`
|
||||
|
||||
- via email at <hi@moonythm.dev>
|
||||
- on discord as `@prescientmoon`
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
inputs:
|
||||
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [ "x86_64-linux" ];
|
||||
imports = [
|
||||
inputs.flake-parts.flakeModules.easyOverlay
|
||||
./nix
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
|
@ -33,6 +38,7 @@
|
|||
|
||||
buildInputs = with pkgs; [ ];
|
||||
|
||||
MOONYTYM_DRAFTS = 1;
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
||||
};
|
||||
};
|
||||
|
|
3
justfile
3
justfile
|
@ -1,3 +1,6 @@
|
|||
default:
|
||||
@just --list
|
||||
|
||||
minify-sitemap:
|
||||
xmllint --noblanks dist/sitemap.xml --output dist/sitemap.xml
|
||||
|
||||
|
|
1
last_modified.toml
Normal file
1
last_modified.toml
Normal file
|
@ -0,0 +1 @@
|
|||
pages = [["Posts", "2024-11-07T08:50:05+01:00"], [{ Post = "arcaea-pookies" }, "2025-03-03T21:33:19.352199797Z"], [{ Post = "the-realm-s-secrets" }, "2024-11-06T08:34:21+01:00"], [{ Post = "arcaea" }, "2024-11-08T08:52:29+01:00"], [{ Post = "yugioh-my-beloved" }, "2024-11-06T08:34:21+01:00"], [{ Post = "games" }, "2024-12-01T18:25:55+01:00"], ["NotFound", "2024-11-07T08:50:05+01:00"], ["Home", "2024-11-06T04:26:30+01:00"]]
|
15
nix/default.nix
Normal file
15
nix/default.nix
Normal file
|
@ -0,0 +1,15 @@
|
|||
{ ... }:
|
||||
{
|
||||
perSystem =
|
||||
{ config, final, ... }:
|
||||
{
|
||||
packages = {
|
||||
moonythm-generator = final.callPackage (import ./moonythm-generator.nix) { };
|
||||
moonythm = final.callPackage (import ./moonythm.nix) { };
|
||||
};
|
||||
|
||||
overlayAttrs = {
|
||||
inherit (config.packages) moonythm-generator moonythm;
|
||||
};
|
||||
};
|
||||
}
|
37
nix/moonythm-generator.nix
Normal file
37
nix/moonythm-generator.nix
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "moonythm-generator";
|
||||
version = "unstable-2025-03-03";
|
||||
src = lib.fileset.toSource {
|
||||
root = ../.;
|
||||
fileset = lib.fileset.unions [
|
||||
../Cargo.lock
|
||||
../Cargo.toml
|
||||
../src
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ ];
|
||||
buildInputs = [ ];
|
||||
|
||||
useFetchCargoVendor = true;
|
||||
cargoLock = {
|
||||
lockFile = ../Cargo.lock;
|
||||
outputHashes = {
|
||||
"tree-sitter-djot-2.0.0" = "sha256-7qwBdueO33SdOp5KY12WMIkDgjS5Psz2eF804wn/aLk=";
|
||||
};
|
||||
};
|
||||
|
||||
# Disable all tests
|
||||
doCheck = false;
|
||||
|
||||
meta = {
|
||||
description = "Static site generator tailor-made for moonythm.dev";
|
||||
homepage = "https://git.moonythm.dev/prescientmoon/moonythm";
|
||||
mainProgram = "moonythm";
|
||||
platforms = [ "x86_64-linux" ];
|
||||
};
|
||||
}
|
10
nix/moonythm.nix
Normal file
10
nix/moonythm.nix
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
moonythm-generator,
|
||||
runCommand,
|
||||
}:
|
||||
runCommand "moonythm" { } ''
|
||||
mkdir $out
|
||||
|
||||
cd ${../.}
|
||||
MOONYTHM_OUT_DIR="$out" ${moonythm-generator}/bin/moonythm
|
||||
''
|
|
@ -30,15 +30,6 @@ h6 {
|
|||
/* Note: I need to check whether this only aligns things better with the font I use */
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Neuromoprhism */
|
||||
/* &:has(.heading-anchor) { */
|
||||
/* padding: 0.5rem 1rem; */
|
||||
/* border-radius: 6px; */
|
||||
/* box-shadow: */
|
||||
/* 3px 3px 6px #cccbc9, */
|
||||
/* -3px -3px 10px white; */
|
||||
/* } */
|
||||
}
|
||||
|
||||
math[display="block"] {
|
||||
|
@ -80,9 +71,6 @@ pre > code {
|
|||
}
|
||||
|
||||
.aside {
|
||||
/* box-shadow: */
|
||||
/* inset 3px 3px 6px #cccbc9, */
|
||||
/* inset -3px -3px 6px white; */
|
||||
background: #faebff;
|
||||
border-radius: 3px;
|
||||
|
||||
|
@ -230,13 +218,6 @@ pre > code {
|
|||
border: 1px solid;
|
||||
background: #eff1f5;
|
||||
|
||||
/* background: #faebff; */
|
||||
|
||||
/* Neuromoprhism */
|
||||
/* box-shadow: */
|
||||
/* inset 3px 3px 6px #cccbc9, */
|
||||
/* inset -3px -3px 6px white; */
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
|
@ -259,6 +240,7 @@ pre > code {
|
|||
|
||||
ol ol {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
@ -272,11 +254,34 @@ pre > code {
|
|||
}
|
||||
}
|
||||
/* }}} */
|
||||
/* {{{ Figure */
|
||||
figure {
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
figure img {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
figure > figcaption {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figure > figcaption > p {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
/* }}} */
|
||||
/* {{{ Light theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
code {
|
||||
background: #eff1f5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.aside code {
|
||||
background: #e4e9f5;
|
||||
}
|
||||
|
||||
/* {{{ Syntax highlighting */
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::metadata::PageMetadata;
|
||||
use crate::metadata::{LastModifiedCache, PageMetadata};
|
||||
use crate::template;
|
||||
use anyhow::{bail, Context};
|
||||
|
||||
|
@ -16,6 +16,7 @@ pub fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
|||
pub struct Pages<'a> {
|
||||
pages: Vec<PageMetadata<'a>>,
|
||||
assets: Vec<PathBuf>,
|
||||
pub last_modified_cache: LastModifiedCache,
|
||||
|
||||
content_root: PathBuf,
|
||||
out_root: PathBuf,
|
||||
|
@ -23,14 +24,15 @@ pub struct Pages<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Pages<'a> {
|
||||
pub fn new(content_root: PathBuf, out_root: PathBuf) -> Self {
|
||||
Self {
|
||||
pub fn new(content_root: PathBuf, out_root: PathBuf) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
last_modified_cache: LastModifiedCache::from_file()?,
|
||||
pages: Vec::new(),
|
||||
assets: Vec::new(),
|
||||
content_root,
|
||||
out_root,
|
||||
base_url: "http://localhost:3000",
|
||||
}
|
||||
base_url: "http://localhost:8080",
|
||||
})
|
||||
}
|
||||
|
||||
// {{{ Collect simple pages
|
||||
|
@ -48,7 +50,8 @@ impl<'a> Pages<'a> {
|
|||
let source = Box::leak(Box::new(source));
|
||||
|
||||
let events = jotdown::Parser::new(source);
|
||||
let metadata = PageMetadata::new(content_path, source, events)?;
|
||||
let metadata =
|
||||
PageMetadata::new(&mut self.last_modified_cache, content_path, source, events)?;
|
||||
|
||||
self.pages.push(metadata);
|
||||
|
||||
|
@ -87,6 +90,30 @@ impl<'a> Pages<'a> {
|
|||
|
||||
page_renderer.feed(&mut out, |label, out| {
|
||||
match label {
|
||||
"title" => {
|
||||
let mut w = crate::html::Writer::new(page, &[], self.base_url);
|
||||
w.states.push(crate::html::State::TextOnly);
|
||||
|
||||
for event in &page.title.events {
|
||||
w.render_event(event, out)?;
|
||||
}
|
||||
}
|
||||
"description" => {
|
||||
let mut w = crate::html::Writer::new(page, &[], self.base_url);
|
||||
w.states.push(crate::html::State::TextOnly);
|
||||
|
||||
for event in &page.description {
|
||||
w.render_event(event, out)?;
|
||||
}
|
||||
}
|
||||
"url" => {
|
||||
write!(
|
||||
out,
|
||||
"{}/{}",
|
||||
self.base_url,
|
||||
page.route.to_path().to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
"content" => {
|
||||
let mut w = crate::html::Writer::new(page, &self.pages, self.base_url);
|
||||
|
||||
|
@ -132,16 +159,17 @@ impl<'a> Pages<'a> {
|
|||
)?;
|
||||
|
||||
for page in &self.pages {
|
||||
if page.config.sitemap_exclude {
|
||||
if page.config.sitemap_exclude || page.config.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(
|
||||
out,
|
||||
"<url>
|
||||
<loc>https://moonythm.dev/{}</loc>
|
||||
<loc>{}/{}</loc>
|
||||
<lastmod>{}</lastmod>
|
||||
",
|
||||
self.base_url,
|
||||
page.route.to_path().to_str().unwrap(),
|
||||
page.last_modified.to_rfc3339()
|
||||
)?;
|
||||
|
|
225
src/html.rs
225
src/html.rs
|
@ -29,8 +29,8 @@ use crate::template;
|
|||
use crate::template::TemplateRenderer;
|
||||
|
||||
pub struct Writer<'s> {
|
||||
pub states: Vec<State<'s>>,
|
||||
list_tightness: Vec<bool>,
|
||||
states: Vec<State<'s>>,
|
||||
footnotes: Footnotes<'s>,
|
||||
metadata: &'s PageMetadata<'s>,
|
||||
pages: &'s [PageMetadata<'s>],
|
||||
|
@ -38,10 +38,11 @@ pub struct Writer<'s> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum State<'s> {
|
||||
pub enum State<'s> {
|
||||
TextOnly,
|
||||
Ignore,
|
||||
Raw,
|
||||
Figure,
|
||||
Math(bool),
|
||||
CodeBlock(String),
|
||||
Aside(TemplateRenderer<'s>),
|
||||
|
@ -93,7 +94,10 @@ impl<'s> Writer<'s> {
|
|||
Event::End(Container::Image(..)) => {
|
||||
self.states.pop();
|
||||
}
|
||||
Event::Str(s) => write!(out, "{}", Escaped(s))?,
|
||||
Event::Str(s) => {
|
||||
write!(out, "{}", Escaped(s))?;
|
||||
return Ok(());
|
||||
}
|
||||
_ => return Ok(()),
|
||||
}
|
||||
}
|
||||
|
@ -277,10 +281,17 @@ impl<'s> Writer<'s> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Skip hidden pages
|
||||
if post.config.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip drafts
|
||||
// if post.config.created_at.is_none() {
|
||||
// continue;
|
||||
// }
|
||||
if std::env::var("MOONYTHM_DRAFTS").unwrap_or_default() != "1"
|
||||
&& post.config.created_at.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
template!("templates/post-summary.html", out)?.feed(
|
||||
out,
|
||||
|
@ -304,7 +315,12 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
if !Self::write_metadata_label(out, label, post)? {
|
||||
if !Self::write_metadata_label(
|
||||
out,
|
||||
label,
|
||||
post,
|
||||
self.base_url,
|
||||
)? {
|
||||
bail!("Unknown label {label} in `post-summary` template");
|
||||
};
|
||||
}
|
||||
|
@ -389,6 +405,23 @@ impl<'s> Writer<'s> {
|
|||
self.states.push(State::Ignore);
|
||||
}
|
||||
// }}}
|
||||
// {{{ Figure
|
||||
Container::Div { class: "figure" } => {
|
||||
self.states.push(State::Figure);
|
||||
let alt = attrs.get_value("alt").ok_or_else(|| {
|
||||
anyhow!("Figure element encountered without an `alt` attribute")
|
||||
})?;
|
||||
let src = attrs.get_value("src").ok_or_else(|| {
|
||||
anyhow!("Figure element encountered without a `src` attribute")
|
||||
})?;
|
||||
|
||||
write!(out, r#"<figure><img alt=""#)?;
|
||||
write_attribute(out, &alt)?;
|
||||
write!(out, r#"" src=""#)?;
|
||||
write_attribute(out, &src)?;
|
||||
write!(out, r#""><figcaption>"#)?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Div
|
||||
Container::Div { class } => {
|
||||
if has_role(attrs, "description") {
|
||||
|
@ -515,6 +548,17 @@ impl<'s> Writer<'s> {
|
|||
renderer.finish(out)?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Figure
|
||||
Container::Div { class: "figure" } => {
|
||||
let State::Figure = self.states.pop().unwrap() else {
|
||||
panic!(
|
||||
"Arrived at end of figure without being in the approriate state."
|
||||
);
|
||||
};
|
||||
|
||||
write!(out, "</figcaption></figure>")?;
|
||||
}
|
||||
// }}}
|
||||
Container::Heading { level, .. } => {
|
||||
write!(out, "</h{}>", level)?;
|
||||
|
||||
|
@ -522,9 +566,12 @@ impl<'s> Writer<'s> {
|
|||
if let Some(State::Article(renderer)) = self.states.last_mut() {
|
||||
if renderer.current() == Some("title") {
|
||||
while let Some(label) = renderer.next(out)? {
|
||||
if !Self::write_common_label(out, label, self.base_url)?
|
||||
&& !Self::write_metadata_label(out, label, self.metadata)?
|
||||
{
|
||||
if !Self::write_metadata_label(
|
||||
out,
|
||||
label,
|
||||
self.metadata,
|
||||
self.base_url,
|
||||
)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -532,7 +579,9 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
// }}}
|
||||
}
|
||||
Container::Image(src, ..) => write!(out, r#" {}>"#, Attr("src", src))?,
|
||||
Container::Image(src, ..) => {
|
||||
write!(out, r#"" {}>"#, Attr("src", src))?;
|
||||
}
|
||||
Container::Blockquote => out.write_str("</blockquote>")?,
|
||||
Container::ListItem { .. } => out.write_str("</li>")?,
|
||||
Container::DescriptionList => out.write_str("</dl>")?,
|
||||
|
@ -550,15 +599,42 @@ impl<'s> Writer<'s> {
|
|||
panic!("Arrived at end of code block without being in the approriate state.");
|
||||
};
|
||||
|
||||
if *language == "rust" {
|
||||
let mut highlighter = Highlighter::new();
|
||||
let language = Language::new(tree_sitter_rust::LANGUAGE);
|
||||
|
||||
let mut config = HighlightConfiguration::new(
|
||||
language,
|
||||
let grammar = match *language {
|
||||
"rust" => Some((
|
||||
"rust",
|
||||
Language::new(tree_sitter_rust::LANGUAGE),
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_rust::INJECTIONS_QUERY,
|
||||
)),
|
||||
"djot" => Some((
|
||||
"dj",
|
||||
tree_sitter_djot::language(),
|
||||
tree_sitter_djot::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_djot::INJECTIONS_QUERY,
|
||||
)),
|
||||
"html" => Some((
|
||||
"html",
|
||||
Language::new(tree_sitter_html::LANGUAGE),
|
||||
tree_sitter_html::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_html::INJECTIONS_QUERY,
|
||||
)),
|
||||
// "tex" => Some((
|
||||
// "tex",
|
||||
// crate::bindings::tree_sitter_latex::language(),
|
||||
// "",
|
||||
// "",
|
||||
// )),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((ft, ts_language, highlights, injections)) = grammar {
|
||||
let mut highlighter = Highlighter::new();
|
||||
|
||||
let mut config = HighlightConfiguration::new(
|
||||
ts_language,
|
||||
ft,
|
||||
highlights,
|
||||
injections,
|
||||
"",
|
||||
)?;
|
||||
|
||||
|
@ -763,65 +839,75 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Fill in metadata labels
|
||||
fn write_common_label(
|
||||
out: &mut impl std::fmt::Write,
|
||||
label: &str,
|
||||
base_url: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
if label == "base_url" {
|
||||
write!(out, "{}", base_url)?;
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn write_metadata_label(
|
||||
out: &mut impl std::fmt::Write,
|
||||
label: &str,
|
||||
meta: &PageMetadata,
|
||||
base_url: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
if label == "posted_on" {
|
||||
if let Some(d) = meta.config.created_at {
|
||||
write!(out, "Posted on ")?;
|
||||
write_datetime(out, &d)?;
|
||||
} else {
|
||||
write!(out, "Being conjured by ")?;
|
||||
match label {
|
||||
"posted_on" => {
|
||||
if let Some(d) = meta.config.created_at {
|
||||
write!(out, "Posted on ")?;
|
||||
write_datetime(out, &d)?;
|
||||
} else {
|
||||
write!(out, "Being conjured ")?;
|
||||
}
|
||||
}
|
||||
} else if label == "updated_on" {
|
||||
write_datetime(out, &meta.last_modified)?;
|
||||
} else if label == "word_count" {
|
||||
let wc = meta.word_count;
|
||||
if wc < 400 {
|
||||
write!(out, "{}", wc)?;
|
||||
} else if wc < 1000 {
|
||||
write!(out, "{}", wc / 10 * 10)?;
|
||||
} else if wc < 2000 {
|
||||
write!(out, "{}", wc / 100 * 100)?;
|
||||
} else {
|
||||
write!(out, "{} thousand", wc / 1000)?;
|
||||
"base_url" => {
|
||||
write!(out, "{}", base_url)?;
|
||||
}
|
||||
} else if label == "reading_duration" {
|
||||
let minutes = meta.word_count / 200;
|
||||
if minutes == 0 {
|
||||
let seconds = meta.word_count * 60 / 200;
|
||||
write!(out, "very short {seconds} second")?;
|
||||
} else if minutes < 10 {
|
||||
write!(out, "short {minutes} minute")?;
|
||||
} else if minutes < 20 {
|
||||
write!(out, "somewhat short {minutes} minute")?;
|
||||
} else if minutes < 30 {
|
||||
write!(out, "somewhat long {minutes}")?;
|
||||
} else if minutes < 60 {
|
||||
write!(out, "long {minutes}")?;
|
||||
} else {
|
||||
let hours = minutes / 60;
|
||||
let minutes = minutes % 60;
|
||||
write!(out, "very long {hours} hour and {minutes} minute")?;
|
||||
"updated_on" => {
|
||||
write_datetime(out, &meta.last_modified)?;
|
||||
}
|
||||
"word_count" => {
|
||||
let wc = meta.word_count;
|
||||
if wc < 400 {
|
||||
write!(out, "{}", wc)?;
|
||||
} else if wc < 1000 {
|
||||
write!(out, "{}", wc / 10 * 10)?;
|
||||
} else if wc < 2000 {
|
||||
write!(out, "{}", wc / 100 * 100)?;
|
||||
} else {
|
||||
write!(out, "{} thousand", wc / 1000)?;
|
||||
}
|
||||
}
|
||||
"reading_duration" => {
|
||||
let minutes = meta.word_count / 200;
|
||||
if minutes == 0 {
|
||||
let seconds = meta.word_count * 60 / 200;
|
||||
write!(out, "very short {seconds} second")?;
|
||||
} else if minutes < 10 {
|
||||
write!(out, "short {minutes} minute")?;
|
||||
} else if minutes < 20 {
|
||||
write!(out, "somewhat short {minutes} minute")?;
|
||||
} else if minutes < 30 {
|
||||
write!(out, "somewhat long {minutes}")?;
|
||||
} else if minutes < 60 {
|
||||
write!(out, "long {minutes}")?;
|
||||
} else {
|
||||
let hours = minutes / 60;
|
||||
let minutes = minutes % 60;
|
||||
write!(out, "very long {hours} hour and {minutes} minute")?;
|
||||
}
|
||||
}
|
||||
"source_url" => {
|
||||
write!(
|
||||
out,
|
||||
"https://git.moonythm.dev/prescientmoon/moonythm/src/branch/main/{}",
|
||||
meta.source_path.to_str().unwrap()
|
||||
)?;
|
||||
}
|
||||
"changelog_url" => {
|
||||
write!(
|
||||
out,
|
||||
"https://git.moonythm.dev/prescientmoon/moonythm/commits/branch/main/{}",
|
||||
meta.source_path.to_str().unwrap()
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
@ -878,10 +964,11 @@ fn write_datetime<T: TimeZone>(
|
|||
datetime: &DateTime<T>,
|
||||
) -> std::fmt::Result {
|
||||
let datetime = datetime.to_utc();
|
||||
|
||||
write!(
|
||||
out,
|
||||
r#"<time datetime="{}">{}</time>"#,
|
||||
datetime.to_rfc3339(),
|
||||
datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, false),
|
||||
datetime.format("%a, %d %b %Y")
|
||||
)
|
||||
}
|
||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Context;
|
||||
use metadata::should_refresh_last_modified;
|
||||
|
||||
mod generate;
|
||||
mod html;
|
||||
|
@ -10,7 +11,11 @@ mod template;
|
|||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let public_path = PathBuf::from_str("public")?;
|
||||
let dist_path = PathBuf::from_str("dist")?;
|
||||
let dist_path = PathBuf::from_str(
|
||||
std::env::var("MOONYTHM_OUT_DIR")
|
||||
.as_deref()
|
||||
.unwrap_or("dist"),
|
||||
)?;
|
||||
|
||||
if dist_path.exists() {
|
||||
std::fs::remove_dir_all(&dist_path).with_context(|| "Cannot delete `dist` directory")?;
|
||||
|
@ -18,14 +23,13 @@ fn main() -> anyhow::Result<()> {
|
|||
|
||||
std::fs::create_dir(&dist_path).with_context(|| "Cannot create `dist` directory")?;
|
||||
|
||||
let mut page = generate::Pages::new(
|
||||
PathBuf::from_str("content").unwrap(),
|
||||
PathBuf::from_str("dist").unwrap(),
|
||||
);
|
||||
let mut pages = generate::Pages::new(PathBuf::from_str("content").unwrap(), dist_path.clone())?;
|
||||
|
||||
page.add_dir(&PathBuf::from_str("")?)
|
||||
pages
|
||||
.add_dir(&PathBuf::from_str("")?)
|
||||
.with_context(|| "Failed to collect directories")?;
|
||||
page.generate()
|
||||
pages
|
||||
.generate()
|
||||
.with_context(|| "Failed to generate markup")?;
|
||||
|
||||
for p in std::fs::read_dir(public_path)? {
|
||||
|
@ -33,5 +37,9 @@ fn main() -> anyhow::Result<()> {
|
|||
.with_context(|| "Cannot copy `public` -> `dist`")?;
|
||||
}
|
||||
|
||||
if should_refresh_last_modified() {
|
||||
pages.last_modified_cache.save()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ use chrono::{DateTime, FixedOffset, Utc};
|
|||
use jotdown::{Attributes, Container, Event};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn should_refresh_last_modified() -> bool {
|
||||
std::env::var("MOONYTHM_UPDATE_LAST_MODIFIED").unwrap_or_default() == "1"
|
||||
}
|
||||
|
||||
// {{{ Config
|
||||
#[derive(Deserialize, Debug, Default)]
|
||||
pub struct PageConfig {
|
||||
|
@ -18,6 +22,9 @@ pub struct PageConfig {
|
|||
|
||||
#[serde(default)]
|
||||
pub sitemap_exclude: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl PageConfig {
|
||||
|
@ -61,6 +68,7 @@ impl PageConfig {
|
|||
)?;
|
||||
|
||||
self.sitemap_exclude |= other.sitemap_exclude;
|
||||
self.hidden |= other.hidden;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -68,7 +76,7 @@ impl PageConfig {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Routing
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PageRoute {
|
||||
Home,
|
||||
NotFound,
|
||||
|
@ -134,6 +142,7 @@ pub struct Heading<'a> {
|
|||
pub struct PageMetadata<'s> {
|
||||
pub config: PageConfig,
|
||||
pub route: PageRoute,
|
||||
pub source_path: PathBuf,
|
||||
|
||||
pub title: Heading<'s>,
|
||||
pub description: Vec<jotdown::Event<'s>>,
|
||||
|
@ -147,29 +156,46 @@ pub struct PageMetadata<'s> {
|
|||
|
||||
impl<'a> PageMetadata<'a> {
|
||||
pub fn new(
|
||||
last_modified_cache: &mut LastModifiedCache,
|
||||
path: PathBuf,
|
||||
source: &'a str,
|
||||
mut events: impl Iterator<Item = Event<'a>>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let last_modified_output = Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg(r#"--pretty=format:%cI"#)
|
||||
.arg(&path)
|
||||
.output()
|
||||
.with_context(|| anyhow!("Could not read the last modification date for file"))?
|
||||
.stdout;
|
||||
let last_modified = String::from_utf8(last_modified_output)?;
|
||||
let route = PageRoute::from_path(&path)?;
|
||||
let last_modified = if should_refresh_last_modified() {
|
||||
let last_modified_output = Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg(r#"--pretty=format:%cI"#)
|
||||
.arg(&path)
|
||||
.output()
|
||||
.with_context(|| anyhow!("Could not read the last modification date for file"))?
|
||||
.stdout;
|
||||
let last_modified = String::from_utf8(last_modified_output)?;
|
||||
|
||||
let last_modified = if last_modified.is_empty() {
|
||||
Utc::now().fixed_offset()
|
||||
let last_modified = if last_modified.is_empty() {
|
||||
Utc::now().fixed_offset()
|
||||
} else {
|
||||
DateTime::parse_from_rfc3339(&last_modified).with_context(|| {
|
||||
anyhow!(
|
||||
"Failed to parse datetime returned by git '{}'",
|
||||
last_modified
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
last_modified_cache
|
||||
.pages
|
||||
.push((route.clone(), last_modified));
|
||||
|
||||
last_modified
|
||||
} else {
|
||||
DateTime::parse_from_rfc3339(&last_modified).with_context(|| {
|
||||
anyhow!(
|
||||
"Failed to parse datetime returned by git '{}'",
|
||||
last_modified
|
||||
)
|
||||
})?
|
||||
last_modified_cache
|
||||
.pages
|
||||
.iter()
|
||||
.find(|item| item.0 == route)
|
||||
.map(|(_, last_modified)| *last_modified)
|
||||
.unwrap_or_else(|| Utc::now().fixed_offset())
|
||||
};
|
||||
|
||||
let mut w = Writer::new();
|
||||
|
@ -181,9 +207,10 @@ impl<'a> PageMetadata<'a> {
|
|||
.ok_or_else(|| anyhow!("No heading found to infer title from"))?;
|
||||
|
||||
Ok(Self {
|
||||
route: PageRoute::from_path(&path)?,
|
||||
route,
|
||||
title: title.clone(),
|
||||
last_modified,
|
||||
source_path: path,
|
||||
source,
|
||||
config: w.config,
|
||||
description: w.description,
|
||||
|
@ -303,3 +330,28 @@ pub fn has_role(attrs: &Attributes, value: &str) -> bool {
|
|||
.map_or(false, |role| format!("{role}") == value)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Last modified cache
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct LastModifiedCache {
|
||||
pages: Vec<(PageRoute, DateTime<FixedOffset>)>,
|
||||
}
|
||||
|
||||
impl LastModifiedCache {
|
||||
pub fn from_file() -> anyhow::Result<LastModifiedCache> {
|
||||
if should_refresh_last_modified() {
|
||||
Ok(Self::default())
|
||||
} else {
|
||||
Ok(toml::de::from_str(&std::fs::read_to_string(
|
||||
"last_modified.toml",
|
||||
)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
Ok(std::fs::write(
|
||||
"last_modified.toml",
|
||||
toml::ser::to_string(self)?,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
|
|
@ -23,6 +23,15 @@
|
|||
as="font"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<!-- Meta -->
|
||||
<meta property="og:site_name" content="Moonythm" />
|
||||
<meta property="og:title" content="{{title}}" />
|
||||
<meta property="twitter:title" content="{{title}}" />
|
||||
<meta property="og:description" content="{{description}}" />
|
||||
<meta property="twitter:description" content="{{description}}" />
|
||||
<meta property="og:url" content="{{url}}" />
|
||||
<meta name="fediverse:creator" content="@prescientmoon@social.moonythm.dev" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</h2>
|
||||
|
||||
<ul>
|
||||
<li>{{posted_on}} by <a href="about:blank">prescientmoon</a></li>
|
||||
<li>{{posted_on}} by <a href="{{base_url}}">prescientmoon</a></li>
|
||||
<li>Last updated on {{updated_on}}.</li>
|
||||
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
||||
</ul>
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
|
||||
<ul>
|
||||
<li>
|
||||
{{posted_on}} by <a href="about:blank">prescientmoon</a> on their
|
||||
<a href="{{base_url}}">website</a>
|
||||
{{posted_on}} by <a href="base_url">prescientmoon</a>
|
||||
</li>
|
||||
<li>
|
||||
Last updated on {{updated_on}}. <a href="about:blank">Source</a> |
|
||||
<a href="about:blank">Changelog</a>
|
||||
Last updated on {{updated_on}}. <a href="{{source_url}}">Source</a> |
|
||||
<a href="{{changelog_url}}">Changelog</a>
|
||||
</li>
|
||||
<li>About {{word_count}} words; a {{reading_duration}} read</li>
|
||||
</ul>
|
||||
|
|
Loading…
Reference in a new issue