404 page, sitemap, and some linters
This commit is contained in:
parent
5d65037b26
commit
d604ca637d
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@ target
|
|||
dist
|
||||
oldicons
|
||||
result
|
||||
node_modules
|
||||
tmp
|
||||
|
|
79
Cargo.lock
generated
79
Cargo.lock
generated
|
@ -38,6 +38,21 @@ version = "1.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
|
@ -80,12 +95,51 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
|
@ -169,11 +223,13 @@ name = "moonythm"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"chrono",
|
||||
"jotdown",
|
||||
"once_cell",
|
||||
"pulldown-latex",
|
||||
"serde",
|
||||
"sha2",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"tree-sitter-highlight",
|
||||
|
@ -280,6 +336,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
@ -399,12 +466,24 @@ dependencies = [
|
|||
"tree-sitter-language",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.95"
|
||||
|
|
|
@ -17,3 +17,5 @@ chrono = { version = "0.4.38", features = ["serde"] }
|
|||
tree-sitter = "0.24.3"
|
||||
tree-sitter-rust = "0.23.0"
|
||||
tree-sitter-highlight = "0.24.3"
|
||||
sha2 = "0.10.8"
|
||||
base64 = "0.22.1"
|
||||
|
|
10
content/404.dj
Normal file
10
content/404.dj
Normal file
|
@ -0,0 +1,10 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
sitemap_exclude = true
|
||||
```
|
||||
|
||||
# Ooooops
|
||||
|
||||
This page does not seem to exist. If this page was here before, please [contact me][Page containing my contact info].
|
||||
|
||||
[Page containing my contact info]: /
|
|
@ -23,7 +23,7 @@ A discussion on pretty much every aspect of the mobile rhythm game "Arcaea".
|
|||
{% [[[ %}
|
||||
The game judges the way the player hits (or fails to do so) every note by quantizing the inputs into [a few judgements](https://arcaea.fandom.com/wiki/Scoring): a PURE judgement is awarded within a 50ms timing window, the more lenient FAR judgement has twice as much (100ms), with all the other notes being marked as LOST. Moreover, an optional tighter (25ms) timing window awards the MAX PURE (usually referred to as "shiny pure" by the community) judgement.
|
||||
|
||||
{ title="Lagrange's extras: Ghost tapping" character="lagrange" }
|
||||
{ title="Lagrange's extras: Ghost tapping" character="lagrange" id="ghost-tapping" }
|
||||
::: char-aside
|
||||
The game only awards judgements for notes in the chart. That is, tapping while no notes are nearby will not influence the score in any way! This turns out to be quite useful, leading to what is referred to as "ghost tapping", which can be a useful way to keep the rhythm or have fun with the chart. An example of this in action can be seen in [this clip][A clip of @gba553 PMing ℵ~0~] of [@gba553](https://www.youtube.com/@Gba553) timing the final pair of tricky notes in [ℵ~0~][The ℵ~0~ chart].
|
||||
|
||||
|
@ -37,7 +37,7 @@ The score is then calculated using a 2:1 PURE to FAR ratio (that is, every PURE
|
|||
|
||||
A play with no LOST judgements is called a "full recall" (also known as a "full combo" in other games). Furthermore, a play with no FAR judgements is called a "pure memory" (also known as an "all perfect" in other games, and commonly abbreviated as a "PM"). Finally, a play where every note was hit perfectly (that is, one where everything is a MAX PURE) is called a "max pure memory" (commonly abbreviated as "MPM" by the community).
|
||||
|
||||
{title="Lagrange's extras: The scoring formula" character="lagrange"}
|
||||
{title="Lagrange's extras: The scoring formula" character="lagrange" id="scoring-formula" }
|
||||
::: char-aside
|
||||
Let's write the score formula in a nice, closed form!
|
||||
|
||||
|
@ -48,7 +48,7 @@ $$`m + \left\lfloor (2(m + p) + f) \frac{10'000'000}{2(m + p + f + l)} \right\rf
|
|||
|
||||
While full recalls and max-PMs are certainly celebrated by players and the larger community alike, PMs are usually the sweet spot between accurate play and fun which many Arcaea players strive for. This is usually the case because the game hardly rewards (if acknowledge at all) plays that are better than a PM (this will become obvious when we discuss the game's rating system).
|
||||
|
||||
{title="Lagrange's extras: ζ-scoring" character="lagrange"}
|
||||
{title="Lagrange's extras: ζ-scoring" character="lagrange" id="ex-scoring"}
|
||||
::: long-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][] 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.
|
||||
|
||||
|
|
|
@ -68,10 +68,10 @@ tiers:
|
|||
</div>
|
||||
</a>
|
||||
<div class="tier-elements">
|
||||
<a href="#arcaea"><img src="icons/arcaea.png" /></a>
|
||||
<a href="#noita"><img src="icons/noita.png" /></a>
|
||||
<a href="#rainworld"><img src="icons/rainworld.png" /></a>
|
||||
<a href="#factorio"><img src="icons/factorio.png" /></a>
|
||||
<a href="#arcaea"><img alt="ooo" src="icons/arcaea.png" /></a>
|
||||
<a href="#noita"><img alt="ooo" src="icons/noita.png" /></a>
|
||||
<a href="#rainworld"><img alt="ooo" src="icons/rainworld.png" /></a>
|
||||
<a href="#factorio"><img alt="ooo" src="icons/factorio.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tier">
|
||||
|
@ -81,9 +81,9 @@ tiers:
|
|||
</div>
|
||||
</a>
|
||||
<div class="tier-elements">
|
||||
<a href="#tunic"><img src="icons/tunic.ico" /></a>
|
||||
<a href="#ultrakill"><img src="icons/ultrakill.png" /></a>
|
||||
<a href="#talos"><img src="icons/thetalosprinciple.jpg" /></a>
|
||||
<a href="#tunic"><img alt="ooo" src="icons/tunic.ico" /></a>
|
||||
<a href="#ultrakill"><img alt="ooo" src="icons/ultrakill.png" /></a>
|
||||
<a href="#talos"><img alt="ooo" src="icons/thetalosprinciple.jpg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tier">
|
||||
|
@ -93,13 +93,13 @@ tiers:
|
|||
</div>
|
||||
</a>
|
||||
<div class="tier-elements">
|
||||
<a href="#celeste"><img src="icons/celeste.png" /></a>
|
||||
<a href="#downpour"><img src="icons/downpour.jpg" /></a>
|
||||
<a href="#hollow-knight"><img src="icons/hollow-knight.png" /></a>
|
||||
<a href="#slay-the-spire"><img src="icons/slaythespire.png" /></a>
|
||||
<a href="#hades"><img src="icons/hades.ico" /></a>
|
||||
<a href="#portal2"><img src="icons/portal2.png" /></a>
|
||||
<a href="#baba"><img src="icons/babaisyou.png" /></a>
|
||||
<a href="#celeste"><img alt="ooo" src="icons/celeste.png" /></a>
|
||||
<a href="#downpour"><img alt="ooo" src="icons/downpour.jpg" /></a>
|
||||
<a href="#hollow-knight"><img alt="ooo" src="icons/hollow-knight.png" /></a>
|
||||
<a href="#slay-the-spire"><img alt="ooo" src="icons/slaythespire.png" /></a>
|
||||
<a href="#hades"><img alt="ooo" src="icons/hades.ico" /></a>
|
||||
<a href="#portal2"><img alt="ooo" src="icons/portal2.png" /></a>
|
||||
<a href="#baba"><img alt="ooo" src="icons/babaisyou.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tier">
|
||||
|
@ -163,15 +163,27 @@ The developers have a (sometimes weekly) [blog](https://www.factorio.com/blog/)
|
|||
I highly recommend not to have your first experience with this game be a co-op session with someone who's played before. Even if the other person is trying their best to let you figure stuff out, they _will_ slip out details or tehniques, if for no other reason than practices feeling _wrong_ to someone who's seen "the light". That's not even mentioning the subset of players whose preferred playstyles involves placing down other people's blueprints. While there's nothing inherently wrong with that, it can take a lot away from one's initial sense of discovery with the game. I'm saying this because I initially bounced away from the game after trying to play with such a friend. In the end, it's up to you.
|
||||
|
||||
{ #arcaea }
|
||||
## Arcaea
|
||||
### Arcaea
|
||||
|
||||
I love rhythm games, and in my humble opinion, [Arcaea](https://arcaea.lowiro.com/) is one of the best ones out there. I'm working on a huge article covering this game extensively, so for now this placeholder paragraph will have to do.
|
||||
|
||||
{ #noita }
|
||||
### Noita
|
||||
|
||||
{ #rainworld }
|
||||
### Rain world
|
||||
|
||||
{ #masterpiece }
|
||||
## Masterpiece
|
||||
|
||||
This tier contains games that are truly special, although haven't implanted themselves in my head for as long (nor as deeply) as the games in the previous tier.
|
||||
|
||||
{ #talos }
|
||||
### The Talos Principle
|
||||
|
||||
{ #tunic }
|
||||
### Tunic
|
||||
|
||||
{ #ultrakill }
|
||||
### Ultrakill
|
||||
|
||||
|
@ -180,3 +192,27 @@ Ultrakill is a game all about doing cool shit. There's so many instances where y
|
|||
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.
|
||||
|
||||
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
|
||||
|
||||
{ #hollow-knight }
|
||||
### Hollow Knight
|
||||
|
||||
{ #celeste }
|
||||
### Celeste
|
||||
|
||||
{ #downpour }
|
||||
### Downpour
|
||||
|
||||
{ #slay-the-spire }
|
||||
### Slay the Spire
|
||||
|
||||
{ #hades }
|
||||
### Hades
|
||||
|
||||
{ #portal2 }
|
||||
### Portal 2
|
||||
|
||||
{ #baba }
|
||||
### Baba is you
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
{ role=config }
|
||||
``` =toml
|
||||
sitemap_changefreq = "weekly"
|
||||
sitemap_priority = 0.7
|
||||
```
|
||||
|
||||
# Echoes
|
||||
|
||||
> _"Remnants of the One who once dwelled within the Silver of the Sky. They now wander endlessly through the mists of the heavens, at times drawn to lost vessels who seek to hear them. Thence, thou with this knowledge are yet to attain the full knowledge of the Plan, but worry not — the World shall linger much longer still."_
|
||||
|
|
11
flake.nix
11
flake.nix
|
@ -11,13 +11,24 @@
|
|||
{
|
||||
devShells.default = pkgs.mkShell rec {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
# Rust tooling
|
||||
pkgs.cargo
|
||||
pkgs.rustc
|
||||
pkgs.clippy
|
||||
pkgs.rust-analyzer
|
||||
pkgs.rustfmt
|
||||
|
||||
# Dev tooling
|
||||
pkgs.imagemagick
|
||||
pkgs.http-server
|
||||
|
||||
# Build / test pipeline
|
||||
pkgs.nodejs
|
||||
pkgs.just
|
||||
pkgs.terser
|
||||
pkgs.libxml2
|
||||
pkgs.validator-nu
|
||||
pkgs.htmltest
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [ ];
|
||||
|
|
41
justfile
Normal file
41
justfile
Normal file
|
@ -0,0 +1,41 @@
|
|||
minify-sitemap:
|
||||
xmllint --noblanks dist/sitemap.xml --output dist/sitemap.xml
|
||||
|
||||
# {{{ Linting
|
||||
lint: lint-vnu lint-css lint-htmltest lint-htmlvalidate
|
||||
|
||||
lint-htmltest:
|
||||
htmltest -c tooling/htmltest.yml dist
|
||||
|
||||
lint-htmlvalidate:
|
||||
#!/usr/bin/env bash
|
||||
shopt -s globstar
|
||||
shopt -s extglob
|
||||
npx --prefix tooling \
|
||||
html-validate -c tooling/htmlvalidate.json dist/**/*.html
|
||||
|
||||
lint-vnu:
|
||||
#!/usr/bin/env bash
|
||||
shopt -s globstar
|
||||
shopt -s extglob
|
||||
|
||||
output=$(
|
||||
vnu --also-check-svg --no-langdetect \
|
||||
--stdout --exit-zero-always \
|
||||
dist/**/*.{html,svg} 2>&1 \
|
||||
| grep -v "Trailing slash on void elements"
|
||||
)
|
||||
|
||||
if [ -n "$output" ]; then
|
||||
echo "$output"
|
||||
exit 1
|
||||
else
|
||||
echo "VNU checks passed succesfully"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
lint-css:
|
||||
npx --prefix tooling stylelint dist/**/*.css \
|
||||
--config ./tooling/stylelintrc.json \
|
||||
--rd --rdd # All disables must come with an explanation and must be necessary
|
||||
# }}}
|
|
@ -5,22 +5,7 @@ body {
|
|||
padding: 1em;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
max-width: 40em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
max-width: 36em;
|
||||
margin: auto;
|
||||
list-style-type: none;
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
/* {{{ General element tweaks*/
|
||||
/* {{{ General element tweaks */
|
||||
blockquote {
|
||||
padding-left: 1.25rem;
|
||||
border-left: 3px solid;
|
||||
|
@ -49,7 +34,23 @@ h6 {
|
|||
math[display="block"] {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
/* }}}*/
|
||||
/* }}} */
|
||||
/* {{{ General article styling */
|
||||
.article-content {
|
||||
max-width: 40em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
max-width: 36em;
|
||||
margin: auto;
|
||||
list-style-type: none;
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
/* }}} */
|
||||
/* {{{ Asides */
|
||||
/* TODO: remove aside-header altogether */
|
||||
.aside-header {
|
||||
|
@ -86,13 +87,13 @@ math[display="block"] {
|
|||
}
|
||||
}
|
||||
|
||||
/* {{{ Override marker*/
|
||||
/* {{{ Override marker */
|
||||
.aside > summary {
|
||||
&::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: "▶";
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.75rem 0 0.25rem;
|
||||
|
@ -100,11 +101,11 @@ math[display="block"] {
|
|||
}
|
||||
}
|
||||
|
||||
.aside[open] > summary:before {
|
||||
.aside[open] > summary::before {
|
||||
content: "▼";
|
||||
}
|
||||
/* }}}*/
|
||||
/* }}}*/
|
||||
/* }}} */
|
||||
/* }}} */
|
||||
/* {{{ Tier lists */
|
||||
.tier-list {
|
||||
background: #444;
|
||||
|
@ -161,7 +162,7 @@ math[display="block"] {
|
|||
}
|
||||
}
|
||||
|
||||
/* {{{ Tier colors*/
|
||||
/* {{{ Tier colors */
|
||||
.tier:nth-child(1) > .tier-heading {
|
||||
background: #b39ddb;
|
||||
}
|
||||
|
@ -202,9 +203,9 @@ math[display="block"] {
|
|||
#e2baff
|
||||
);
|
||||
}
|
||||
/* }}}*/
|
||||
/* }}} */
|
||||
}
|
||||
/* }}}*/
|
||||
/* }}} */
|
||||
|
||||
/* {{{ Light theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
@ -217,11 +218,7 @@ math[display="block"] {
|
|||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* {{{ Syntax highlighting*/
|
||||
span.variable {
|
||||
color: #4c4f69;
|
||||
}
|
||||
|
||||
/* {{{ Syntax highlighting */
|
||||
span.attribute,
|
||||
span.constant {
|
||||
color: #fe640b;
|
||||
|
@ -307,6 +304,6 @@ math[display="block"] {
|
|||
span.variable-parameter {
|
||||
color: #e64553;
|
||||
}
|
||||
/* }}}*/
|
||||
/* }}} */
|
||||
}
|
||||
/* }}} */
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::html::render_html;
|
||||
use crate::metadata::PageMetadata;
|
||||
use crate::template;
|
||||
use anyhow::{bail, Context};
|
||||
|
@ -13,13 +13,26 @@ pub fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Pages<'a> {
|
||||
pages: Vec<PageMetadata<'a>>,
|
||||
assets: Vec<PathBuf>,
|
||||
|
||||
content_root: PathBuf,
|
||||
out_root: PathBuf,
|
||||
base_url: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Pages<'a> {
|
||||
pub fn new(content_root: PathBuf, out_root: PathBuf) -> Self {
|
||||
Self {
|
||||
pages: Vec::new(),
|
||||
assets: Vec::new(),
|
||||
content_root,
|
||||
out_root,
|
||||
base_url: "http://localhost:3000",
|
||||
}
|
||||
}
|
||||
|
||||
// {{{ Collect simple pages
|
||||
pub fn add_page(&mut self, path: &Path) -> anyhow::Result<()> {
|
||||
let content_path = PathBuf::from_str("content")?.join(path);
|
||||
|
@ -63,9 +76,9 @@ impl<'a> Pages<'a> {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Generate
|
||||
pub fn generate(&self, out_root: PathBuf) -> anyhow::Result<()> {
|
||||
pub fn generate(&self) -> anyhow::Result<()> {
|
||||
for page in &self.pages {
|
||||
let out_dir = out_root.join(page.route.to_path());
|
||||
let out_dir = self.out_root.join(page.route.to_path());
|
||||
std::fs::create_dir_all(&out_dir)
|
||||
.with_context(|| format!("Failed to generate {out_dir:?} directory"))?;
|
||||
|
||||
|
@ -75,8 +88,13 @@ impl<'a> Pages<'a> {
|
|||
page_renderer.feed(&mut out, |label, out| {
|
||||
match label {
|
||||
"content" => {
|
||||
let events = jotdown::Parser::new(page.source);
|
||||
render_html(page, &self.pages, events, out)?;
|
||||
let mut w = crate::html::Writer::new(page, &self.pages, self.base_url);
|
||||
|
||||
for event in jotdown::Parser::new(page.source) {
|
||||
w.render_event(&event, out)?;
|
||||
}
|
||||
|
||||
w.render_epilogue(out)?;
|
||||
}
|
||||
_ => bail!("Unknown label {label} in page template"),
|
||||
}
|
||||
|
@ -88,15 +106,62 @@ impl<'a> Pages<'a> {
|
|||
.with_context(|| format!("Failed to write {out_dir:?} post"))?;
|
||||
}
|
||||
|
||||
let content_root = PathBuf::from_str("content")?;
|
||||
for path in &self.assets {
|
||||
std::fs::create_dir_all(out_root.join(path).parent().unwrap())
|
||||
std::fs::create_dir_all(self.out_root.join(path).parent().unwrap())
|
||||
.with_context(|| format!("Failed to create parent dir for asset at {path:?}"))?;
|
||||
|
||||
std::fs::copy(content_root.join(path), out_root.join(path))
|
||||
std::fs::copy(self.content_root.join(path), self.out_root.join(path))
|
||||
.with_context(|| format!("Failed to copy asset at {path:?}"))?;
|
||||
}
|
||||
|
||||
self.generate_sitemap()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// {{{ Generate sitemap
|
||||
fn generate_sitemap(&self) -> anyhow::Result<()> {
|
||||
let mut out = String::new();
|
||||
|
||||
write!(
|
||||
out,
|
||||
r#"
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
"#
|
||||
)?;
|
||||
|
||||
for page in &self.pages {
|
||||
if page.config.sitemap_exclude {
|
||||
continue;
|
||||
}
|
||||
|
||||
write!(
|
||||
out,
|
||||
"<url>
|
||||
<loc>https://moonythm.dev/{}</loc>
|
||||
<lastmod>{}</lastmod>
|
||||
",
|
||||
page.route.to_path().to_str().unwrap(),
|
||||
page.last_modified.to_rfc3339()
|
||||
)?;
|
||||
|
||||
if let Some(priority) = page.config.sitemap_priority {
|
||||
write!(out, "<priority>{priority}</priority>")?;
|
||||
}
|
||||
|
||||
if let Some(changefreq) = &page.config.sitemap_changefreq {
|
||||
write!(out, "<changefreq>{changefreq}</changefreq>")?;
|
||||
}
|
||||
|
||||
write!(out, "</url>")?;
|
||||
}
|
||||
|
||||
write!(out, "</urlset>")?;
|
||||
|
||||
std::fs::write(self.out_root.join("sitemap.xml"), out.trim())
|
||||
.with_context(|| "Failed to write sitemap")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
|
|
188
src/html.rs
188
src/html.rs
|
@ -27,28 +27,13 @@ use crate::metadata::PageRoute;
|
|||
use crate::template;
|
||||
use crate::template::TemplateRenderer;
|
||||
|
||||
// {{{ Renderer
|
||||
/// Render djot content as HTML.
|
||||
pub fn render_html<'s>(
|
||||
metadata: &'s PageMetadata,
|
||||
pages: &'s [PageMetadata],
|
||||
mut events: impl Iterator<Item = Event<'s>>,
|
||||
out: &mut impl std::fmt::Write,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut w = Writer::new(Some(metadata), pages);
|
||||
events.try_for_each(|e| w.render_event(&e, out))?;
|
||||
w.render_epilogue(out)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
|
||||
pub struct Writer<'s> {
|
||||
list_tightness: Vec<bool>,
|
||||
states: Vec<State<'s>>,
|
||||
footnotes: Footnotes<'s>,
|
||||
metadata: Option<&'s PageMetadata<'s>>,
|
||||
metadata: &'s PageMetadata<'s>,
|
||||
pages: &'s [PageMetadata<'s>],
|
||||
base_url: &'s str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -66,13 +51,14 @@ enum State<'s> {
|
|||
}
|
||||
|
||||
impl<'s> Writer<'s> {
|
||||
pub fn new(metadata: Option<&'s PageMetadata>, pages: &'s [PageMetadata]) -> Self {
|
||||
pub fn new(metadata: &'s PageMetadata, pages: &'s [PageMetadata], base_url: &'s str) -> Self {
|
||||
Self {
|
||||
list_tightness: Vec::new(),
|
||||
states: Vec::new(),
|
||||
footnotes: Footnotes::default(),
|
||||
metadata,
|
||||
pages,
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,22 +119,21 @@ impl<'s> Writer<'s> {
|
|||
Event::Start(c, attrs) => {
|
||||
match &c {
|
||||
// {{{ Section
|
||||
Container::Section { id } => match self.metadata {
|
||||
Some(meta) if meta.title.id == *id => {
|
||||
if matches!(meta.route, PageRoute::Post(_)) {
|
||||
Container::Section { id } => {
|
||||
if self.metadata.title.id == *id {
|
||||
if matches!(self.metadata.route, PageRoute::Post(_)) {
|
||||
let mut renderer = template!("templates/post.html", out)?;
|
||||
|
||||
assert_eq!(renderer.current(), Some("attrs"));
|
||||
write!(out, "{}", Attr("aria-labeledby", id))?;
|
||||
write!(out, "{}", Attr("aria-labelledby", id))?;
|
||||
renderer.next(out)?;
|
||||
|
||||
self.states.push(State::Article(renderer));
|
||||
}
|
||||
} else {
|
||||
write!(out, "<section {}>", Attr("aria-labelledby", id))?;
|
||||
}
|
||||
_ => {
|
||||
write!(out, "<section {}>", Attr("aria-labeledby", id))?;
|
||||
}
|
||||
},
|
||||
}
|
||||
// }}}
|
||||
// {{{ Aside
|
||||
Container::Div {
|
||||
|
@ -164,6 +149,13 @@ impl<'s> Writer<'s> {
|
|||
|
||||
while let Some(label) = renderer.current() {
|
||||
match label {
|
||||
"id" => {
|
||||
let id = attrs.get_value("id").ok_or_else(|| {
|
||||
anyhow!("Cannot find `id` attribute on `aside` element")
|
||||
})?;
|
||||
|
||||
write_attribute(out, &id)?;
|
||||
}
|
||||
"character" => {
|
||||
let character =
|
||||
attrs.get_value("character").ok_or_else(|| {
|
||||
|
@ -304,7 +296,7 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
if !fill_metadata_label(out, label, post)? {
|
||||
if !Self::write_metadata_label(out, label, post)? {
|
||||
bail!("Unknown label {label} in `post-summary` template");
|
||||
};
|
||||
}
|
||||
|
@ -343,7 +335,6 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
Container::Math { display } => {
|
||||
self.states.push(State::Math(*display));
|
||||
out.write_str("<math>")?;
|
||||
}
|
||||
Container::Image(_, _) => {
|
||||
self.states.push(State::TextOnly);
|
||||
|
@ -417,12 +408,11 @@ impl<'s> Writer<'s> {
|
|||
// Sanity check
|
||||
assert!(matches!(self.states.last(), Some(State::Math(_))));
|
||||
self.states.pop();
|
||||
out.write_str(r#"</math>"#)?;
|
||||
}
|
||||
// }}}
|
||||
// {{{ Section
|
||||
Container::Section { id, .. } => match self.metadata {
|
||||
Some(meta) if meta.title.id == *id => {
|
||||
Container::Section { id, .. } => {
|
||||
if self.metadata.title.id == *id {
|
||||
if matches!(self.states.last(), Some(State::Article(_))) {
|
||||
let Some(State::Article(renderer)) = self.states.pop() else {
|
||||
unreachable!()
|
||||
|
@ -430,9 +420,10 @@ impl<'s> Writer<'s> {
|
|||
|
||||
renderer.finish(out)?;
|
||||
}
|
||||
} else {
|
||||
out.write_str("</section>")?
|
||||
}
|
||||
_ => out.write_str("</section>")?,
|
||||
},
|
||||
}
|
||||
// }}}
|
||||
// {{{ Aside
|
||||
Container::Div {
|
||||
|
@ -452,11 +443,10 @@ impl<'s> Writer<'s> {
|
|||
// {{{ Article title
|
||||
if let Some(State::Article(renderer)) = self.states.last_mut() {
|
||||
if renderer.current() == Some("title") {
|
||||
// SAFETY: we can never enter into the `article` state without having
|
||||
// some metadata on hand.
|
||||
let meta = self.metadata.unwrap();
|
||||
while let Some(label) = renderer.next(out)? {
|
||||
if !fill_metadata_label(out, label, meta)? {
|
||||
if !Self::write_common_label(out, label, self.base_url)?
|
||||
&& !Self::write_metadata_label(out, label, self.metadata)?
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -658,7 +648,7 @@ impl<'s> Writer<'s> {
|
|||
Event::Hardbreak => out.write_str("<br>")?,
|
||||
Event::Softbreak => out.write_char('\n')?,
|
||||
// }}}
|
||||
Event::ThematicBreak(_) => out.write_str("<hr>")?,
|
||||
Event::ThematicBreak(_) => out.write_str("<hr />")?,
|
||||
Event::Escape | Event::Blankline | Event::Attributes(_) => {}
|
||||
}
|
||||
|
||||
|
@ -666,10 +656,10 @@ impl<'s> Writer<'s> {
|
|||
}
|
||||
|
||||
// {{{ Render epilogue
|
||||
fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
pub fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
|
||||
if self.footnotes.reference_encountered() {
|
||||
// TODO: rewrite this using a template
|
||||
out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
|
||||
out.write_str("<section role=\"doc-endnotes\"><hr/><ol>")?;
|
||||
|
||||
while let Some((number, events)) = self.footnotes.next() {
|
||||
write!(out, r#"<li id="fn{number}">"#)?;
|
||||
|
@ -694,6 +684,72 @@ impl<'s> Writer<'s> {
|
|||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// {{{ 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,
|
||||
) -> 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 ")?;
|
||||
}
|
||||
} 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)?;
|
||||
}
|
||||
} 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")?;
|
||||
}
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// }}}
|
||||
}
|
||||
|
||||
// {{{ HTMl escaper
|
||||
|
@ -812,55 +868,3 @@ impl<'s> Iterator for Footnotes<'s> {
|
|||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Fill in metadata labels
|
||||
fn fill_metadata_label(
|
||||
out: &mut impl std::fmt::Write,
|
||||
label: &str,
|
||||
meta: &PageMetadata<'_>,
|
||||
) -> 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 ")?;
|
||||
}
|
||||
} 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)?;
|
||||
}
|
||||
} 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")?;
|
||||
}
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
|
|
@ -18,10 +18,14 @@ fn main() -> anyhow::Result<()> {
|
|||
|
||||
std::fs::create_dir(&dist_path).with_context(|| "Cannot create `dist` directory")?;
|
||||
|
||||
let mut page = generate::Pages::default();
|
||||
let mut page = generate::Pages::new(
|
||||
PathBuf::from_str("content").unwrap(),
|
||||
PathBuf::from_str("dist").unwrap(),
|
||||
);
|
||||
|
||||
page.add_dir(&PathBuf::from_str("")?)
|
||||
.with_context(|| "Failed to collect directories")?;
|
||||
page.generate(PathBuf::from_str("dist")?)
|
||||
page.generate()
|
||||
.with_context(|| "Failed to generate markup")?;
|
||||
|
||||
for p in std::fs::read_dir(public_path)? {
|
||||
|
|
|
@ -13,30 +13,65 @@ use serde::Deserialize;
|
|||
#[derive(Deserialize, Debug, Default)]
|
||||
pub struct PageConfig {
|
||||
pub created_at: Option<DateTime<FixedOffset>>,
|
||||
pub sitemap_priority: Option<f32>,
|
||||
pub sitemap_changefreq: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub sitemap_exclude: bool,
|
||||
}
|
||||
|
||||
impl PageConfig {
|
||||
/// Merge another config into the current one. Might error out on duplicate values.
|
||||
fn merge(&mut self, other: PageConfig) -> anyhow::Result<()> {
|
||||
match &self.created_at {
|
||||
None => self.created_at = other.created_at,
|
||||
Some(first_created) => {
|
||||
if let Some(second_created) = other.created_at {
|
||||
if second_created != *first_created {
|
||||
bail!("Conflicting values for `created_at` page attribute: {first_created} and {second_created}");
|
||||
// {{{ Merge a single property. Errors out on duplicate values.
|
||||
fn merge_prop<A: PartialEq + std::fmt::Debug>(
|
||||
label: &str,
|
||||
first: Option<A>,
|
||||
second: Option<A>,
|
||||
) -> anyhow::Result<Option<A>> {
|
||||
match first {
|
||||
None => Ok(second),
|
||||
Some(first) => {
|
||||
if let Some(second) = second {
|
||||
if second != first {
|
||||
bail!(
|
||||
"Conflicting values for `{label}` page attribute: {first:?} and {second:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(first))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Config merging
|
||||
/// Merge another config into the current one. Might error out on duplicate values.
|
||||
fn merge(&mut self, other: PageConfig) -> anyhow::Result<()> {
|
||||
self.created_at = Self::merge_prop("created_at", self.created_at, other.created_at)?;
|
||||
|
||||
self.sitemap_priority = Self::merge_prop(
|
||||
"sitemap_priority",
|
||||
self.sitemap_priority,
|
||||
other.sitemap_priority,
|
||||
)?;
|
||||
|
||||
self.sitemap_changefreq = Self::merge_prop(
|
||||
"sitemap_changefreq",
|
||||
self.sitemap_changefreq.take(),
|
||||
other.sitemap_changefreq,
|
||||
)?;
|
||||
|
||||
self.sitemap_exclude |= other.sitemap_exclude;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Routing
|
||||
#[derive(Debug)]
|
||||
pub enum PageRoute {
|
||||
Home,
|
||||
NotFound,
|
||||
Posts,
|
||||
Post(String),
|
||||
}
|
||||
|
@ -50,9 +85,11 @@ impl PageRoute {
|
|||
|
||||
let result = if first == OsStr::new("index.dj") {
|
||||
Self::Home
|
||||
} else if first == OsStr::new("404.dj") {
|
||||
Self::NotFound
|
||||
} else if first == OsStr::new("echoes") {
|
||||
let Some(Component::Normal(second)) = path.components().nth(2) else {
|
||||
bail!("Cannot convert path '{:?}' to page route", path);
|
||||
bail!("Cannot convert path '{:?}' to echo route", path);
|
||||
};
|
||||
let mut slice = second.to_str().unwrap();
|
||||
if slice.ends_with(".dj") {
|
||||
|
@ -75,7 +112,8 @@ impl PageRoute {
|
|||
#[inline]
|
||||
pub fn to_path(&self) -> PathBuf {
|
||||
match self {
|
||||
Self::Home => PathBuf::from_str(".").unwrap(),
|
||||
Self::Home => PathBuf::from_str("").unwrap(),
|
||||
Self::NotFound => PathBuf::from_str("404").unwrap(),
|
||||
Self::Posts => PathBuf::from_str("echoes").unwrap(),
|
||||
Self::Post(id) => PathBuf::from_str(&format!("echoes/{id}")).unwrap(),
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<aside class="aside">{{content}}</aside>
|
||||
<div class="aside">{{content}}</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<aside class="aside">
|
||||
<aside class="aside" aria-labelledby="{{id}}">
|
||||
<div class="aside-header">
|
||||
<img
|
||||
alt="{{character}}"
|
||||
src="/assets/icons/characters/{{character}}.webp"
|
||||
/>
|
||||
<h3>{{title}}</h3>
|
||||
<h3 id="{{id}}">{{title}}</h3>
|
||||
</div>
|
||||
|
||||
{{content}}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<aside>
|
||||
<aside aria-labelledby="{{id}}">
|
||||
<details class="aside aside-long">
|
||||
<summary class="aside-header">
|
||||
<img
|
||||
alt="{{character}}"
|
||||
src="/assets/icons/characters/{{character}}.webp"
|
||||
/>
|
||||
<h3>{{title}}</h3>
|
||||
<h3 id="{{id}}">{{title}}</h3>
|
||||
</summary>
|
||||
{{content}}
|
||||
</details>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<header>
|
||||
<nav>
|
||||
<a href="/"><code>~</code></a> /
|
||||
<a href="/echoes"><code>echoes</code></a>
|
||||
<a href="/echoes/"><code>echoes</code></a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>{{content}}</main>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<li>
|
||||
<article>
|
||||
<h2>
|
||||
<a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
|
||||
<a href="/echoes/{{id}}/" rel="bookmark">{{title}}</a>
|
||||
</h2>
|
||||
|
||||
<ul>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<ul>
|
||||
<li>
|
||||
{{posted_on}} by <a href="about:blank">prescientmoon</a> on their
|
||||
<a href="https://moonythm.dev">website</a>
|
||||
<a href="{{base_url}}">website</a>
|
||||
</li>
|
||||
<li>
|
||||
Last updated on {{updated_on}}. <a href="about:blank">Source</a> |
|
||||
|
@ -17,5 +17,5 @@
|
|||
<hr />
|
||||
</header>
|
||||
|
||||
<section class="article-content">{{content}}</section>
|
||||
<div class="article-content">{{content}}</div>
|
||||
</article>
|
||||
|
|
10
tooling/htmltest.yml
Normal file
10
tooling/htmltest.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
CacheExpires: "168h" # one week
|
||||
CheckFavicon: true
|
||||
EnforceHTML5: true
|
||||
EnforceHTTPS: true
|
||||
ExternalTimeout: 20
|
||||
HTTPConcurrencyLimit: 128
|
||||
IgnoreAltEmpty: true
|
||||
IgnoreHTTPS:
|
||||
- "http://localhost:"
|
||||
IgnoreURLs:
|
9
tooling/htmlvalidate.json
Normal file
9
tooling/htmlvalidate.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": [
|
||||
"html-validate:recommended",
|
||||
"html-validate:standard",
|
||||
"html-validate:a11y",
|
||||
"html-validate:document",
|
||||
"html-validate:prettier"
|
||||
]
|
||||
}
|
1857
tooling/package-lock.json
generated
Normal file
1857
tooling/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
7
tooling/package.json
Normal file
7
tooling/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"html-validate": "^8.24.2",
|
||||
"stylelint": "^16.10.0",
|
||||
"stylelint-config-standard": "^36.0.1"
|
||||
}
|
||||
}
|
8
tooling/stylelintrc.json
Normal file
8
tooling/stylelintrc.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"comment-empty-line-before": null,
|
||||
"no-descending-specificity": null,
|
||||
"declaration-empty-line-before": null
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue