Switch over to WebAssembly, Rust and Yew #35

Merged
BlakeRain merged 87 commits from yew-static into main 2023-08-30 18:01:40 +00:00
40 changed files with 9138 additions and 244 deletions
Showing only changes of commit 469ce60d53 - Show all commits

154
Cargo.lock generated
View File

@ -156,6 +156,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "deranged"
version = "0.3.8"
@ -214,6 +223,16 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "flate2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -725,6 +744,15 @@ version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -753,6 +781,26 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "macros"
version = "0.1.0"
dependencies = [
"gray_matter",
"lazy_static",
"model",
"nom",
"proc-macro2",
"pulldown-cmark",
"quote",
"serde",
"serde_json",
"syn 2.0.29",
"syntect",
"thiserror",
"time",
"words-count",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -791,6 +839,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "model"
version = "2.0.0"
dependencies = [
"serde",
"thiserror",
"time",
]
[[package]]
name = "native-tls"
version = "0.2.11"
@ -853,6 +910,28 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "openssl"
version = "0.10.56"
@ -975,6 +1054,20 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "plist"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
dependencies = [
"base64",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "prettyplease"
version = "0.1.25"
@ -1047,6 +1140,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "quick-xml"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.33"
@ -1168,6 +1270,21 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.22"
@ -1275,11 +1392,10 @@ version = "2.0.0"
dependencies = [
"async-trait",
"env_logger",
"gray_matter",
"include_dir",
"log",
"nom",
"pulldown-cmark",
"macros",
"model",
"reqwest",
"serde",
"serde_json",
@ -1288,7 +1404,6 @@ dependencies = [
"tokio",
"wasm-bindgen-futures",
"wasm-logger",
"words-count",
"yew",
"yew-hooks",
"yew-router",
@ -1352,6 +1467,27 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91"
dependencies = [
"bincode",
"bitflags 1.3.2",
"flate2",
"fnv",
"once_cell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_json",
"thiserror",
"walkdir",
"yaml-rust",
]
[[package]]
name = "tempfile"
version = "3.8.0"
@ -1620,6 +1756,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"

View File

@ -28,20 +28,19 @@ name = "site"
[dependencies]
async-trait = { version = "0.1" }
gray_matter = { version = "0.2", default-features = false, features = ["yaml"] }
include_dir = { version = "0.7" }
log = { version = "0.4" }
nom = { version = "7.1" }
pulldown-cmark = { version = "0.9" }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
thiserror = { version = "1.0" }
words-count = { version = "0.1" }
yew = { version = "0.20" }
yew-hooks = { version = "0.2" }
yew-router = { version = "0.17" }
macros = { path = "./macros" }
model = { path = "./model" }
[dependencies.time]
version = "0.3"
features = [

View File

@ -227,7 +227,7 @@ Oct 27 16:07:12 ip-?-?-? simple-search.py[11080]: 127.0.0.1 - - [27/Oct/2019 16:
Now that I new the API service was in place I needed to configure NGINX so that it would proxy HTTPS from port 9443 to the service port 5000. This meant adding a file in the directory `/etc/nginx/sites-available` that contained the configuration for NGINX. This file also needed to contain the links to the SSL certificate that [Let's Encrypt](https://letsencrypt.org) had set up when Ghost was being installed. Checking in `/etc/letsencrypt` showed a directory called `blakerain.com` that contain the certificate chain and the private key. I could use the default SSL settings from `/etc/nginx/snippets/ssl-params.conf` for the rest.
```nginx
```
server {
listen 9443 ssl http2;
listen [::]:9443 ssl http2;

View File

@ -315,7 +315,7 @@ HugePage::~HugePage() {
When writing the interface with the Ethernet card, I needed to be able to ensure that each huge page was carved up into a number of fixed size buffers. Moreover, these buffers had specific alignment considerations that could vary by device. To facilitate this, I laid out all the buffers in a huge page as follows:
```box-drawing
```plain
A ◀─╴size╶─▶ B ◀─╴size╶─▶ ◀─╴size╶─▶
┌─────┬───┬───┬─────┬───┬─────┬───┬──────────┬───┬──────────┬─────┬──────────┐
│ C │ H │ H │ … │ H │ … │▒▒▒│ Buffer 0 │░░░│ Buffer 1 │ … │ Buffer n │

View File

@ -94,7 +94,7 @@ has a green check, as my website has a link back to my Mastodon profile.
This was achieved by adding a `rel="me"` to the anchor that links back to my Mastodon profile in the navigation header
at the top of this site:
```typescript
```ts
const MastodonLink: FC = () => {
return (
<a

1
macros/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

531
macros/Cargo.lock generated Normal file
View File

@ -0,0 +1,531 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "deranged"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
dependencies = [
"serde",
]
[[package]]
name = "flate2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "gray_matter"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cf2fb99fac0b821a4e61c61abff076324bb0e5c3b4a83815bbc3518a38971ad"
dependencies = [
"serde",
"serde_json",
"yaml-rust",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "macros"
version = "0.1.0"
dependencies = [
"gray_matter",
"lazy_static",
"model",
"nom",
"proc-macro2",
"pulldown-cmark",
"quote",
"serde",
"serde_json",
"syn",
"syntect",
"thiserror",
"time",
"words-count",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "model"
version = "2.0.0"
dependencies = [
"serde",
"thiserror",
"time",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "plist"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
dependencies = [
"base64",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
dependencies = [
"bitflags",
"getopts",
"memchr",
"unicase",
]
[[package]]
name = "quick-xml"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91"
dependencies = [
"bincode",
"bitflags",
"flate2",
"fnv",
"once_cell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_json",
"thiserror",
"walkdir",
"yaml-rust",
]
[[package]]
name = "thiserror"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07"
dependencies = [
"deranged",
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9"
dependencies = [
"time-core",
]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-blocks"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c84398c527c802fbf222e5145f220382d60f1878e0e6cb4d22a3080949a8ddcd"
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "words-count"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c569acc49de9affbc9ace57633381bad6d3e9c648605206d082deb7aa62e8cf5"
dependencies = [
"unicode-blocks",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]

33
macros/Cargo.toml Normal file
View File

@ -0,0 +1,33 @@
[package]
name = "macros"
version = "0.1.0"
publish = false
edition = "2021"
[lib]
proc-macro = true
[dependencies]
lazy_static = { version = "1.4" }
proc-macro2 = { version = "1.0" }
quote = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
syn = "2.0"
thiserror = { version = "1.0" }
# Markdown content parsing
gray_matter = { version = "0.2", default-features = false, features = ["yaml"] }
nom = { version = "7.1" }
pulldown-cmark = { version = "0.9" }
syntect = { version = "5.1", features = ["default-themes", "default-syntaxes"] }
words-count = { version = "0.1" }
model = { path = "../model" }
[dependencies.time]
version = "0.3"
features = [
"formatting",
"parsing",
]

176
macros/src/documents.rs Normal file
View File

@ -0,0 +1,176 @@
use std::path::PathBuf;
use model::document::{Details, Document};
use proc_macro::TokenStream;
use pulldown_cmark::{Options, Parser};
use quote::{quote, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
parse_str, Ident, LitStr,
};
use crate::{error::Error, parse::frontmatter::parse_front_matter};
use self::writer::Writer;
mod highlight;
mod writer;
pub struct DocumentsInput {
pub directory: LitStr,
}
impl Parse for DocumentsInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
directory: input.parse()?,
})
}
}
fn load_documents(directory: &str) -> Result<Vec<Document>, Error> {
let mut root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
root_dir.pop();
root_dir.push(directory);
eprintln!("Loading documents from: {}", root_dir.display());
let mut results = Vec::new();
for entry in std::fs::read_dir(root_dir)? {
let entry = entry?;
let content = std::fs::read(entry.path())?;
let (Some(front_matter), matter) = parse_front_matter(&content[..])? else {
return Err(Error::MissingFrontMatter(entry.path().to_string_lossy().to_string()))
};
let slug = entry
.path()
.file_stem()
.expect("filename")
.to_str()
.expect("valid file name")
.to_string();
let reading_time = words_count::count(&matter.content).words / 200;
results.push(Document {
details: Details::from_front_matter(slug, Some(reading_time), front_matter),
content: matter.content,
})
}
results.sort_by(|a, b| {
b.details
.summary
.published
.cmp(&a.details.summary.published)
});
Ok(results)
}
fn generate_document(
document: Document,
) -> Result<(Ident, proc_macro2::TokenStream, proc_macro2::TokenStream), Error> {
let slug = document.details.summary.slug;
let title = document.details.summary.title;
let excerpt = match document.details.summary.excerpt {
Some(excerpt) => quote! { Some(#excerpt.to_string()) },
None => quote! { None },
};
let published = match document.details.summary.published {
Some(published) => {
let timestamp = published.unix_timestamp();
quote! { Some(time::OffsetDateTime::from_unix_timestamp(#timestamp).unwrap()) }
}
None => quote! { None },
};
let reading_time = match document.details.reading_time {
Some(reading_time) => quote! { Some(#reading_time) },
None => quote! { None },
};
let cover_image = match document.details.cover_image {
Some(cover_image) => quote! { Some(#cover_image.to_string()) },
None => quote! { None },
};
let tags = document
.details
.tags
.into_iter()
.map(|tag| quote! { #tag.to_string() });
// Replace the use of '-' with '_' in the document slug to get the name of the document as a
// Rust identifier.
let name = slug.replace('-', "_");
let doc_ident = parse_str::<Ident>(&(format!("document_{name}"))).expect("document identifier");
let render_ident =
parse_str::<Ident>(&(format!("render_{name}"))).expect("document identifier");
let parser = Parser::new_ext(&document.content, Options::all());
let mut html = String::new();
Writer::new(parser, &mut html).run().expect("HTML");
let doc_func = quote! {
fn #doc_ident() -> Details {
Details {
summary: Summary {
slug: #slug.to_string(),
title: #title.to_string(),
excerpt: #excerpt,
published: #published
},
tags: vec![ #(#tags),* ],
reading_time: #reading_time,
cover_image: #cover_image,
}
}
fn #render_ident() -> yew::Html {
yew::Html::from_html_unchecked(yew::AttrValue::from(#html))
}
};
Ok((
doc_ident.clone(),
doc_func,
quote! {
#slug => Some((#doc_ident(), #render_ident())),
},
))
}
pub fn generate(input: DocumentsInput) -> Result<TokenStream, Error> {
let documents = load_documents(&input.directory.value())?;
let mut combined = quote! {};
let mut doc_funcs = Vec::new();
let mut doc_renders = Vec::new();
for document in documents {
let (doc_ident, doc_func, doc_render) = generate_document(document)?;
combined.append_all(doc_func);
doc_funcs.push(doc_ident);
doc_renders.push(doc_render);
}
Ok(TokenStream::from(quote! {
use model::document::*;
#combined
pub fn documents() -> Vec<Details> {
vec![ #(#doc_funcs()),* ]
}
pub fn render(slug: &str) -> Option<(Details, yew::Html)> {
match slug {
#(#doc_renders)*
_ => None
}
}
}))
}

View File

@ -0,0 +1,26 @@
use lazy_static::lazy_static;
use syntect::{
highlighting::ThemeSet,
parsing::{SyntaxDefinition, SyntaxSet},
};
const TOML_RAW: &str = include_str!("toml.sublime-syntax");
const TYPESCRIPT_RAW: &str = include_str!("typescript.sublime-syntax");
pub fn create_syntaxset() -> SyntaxSet {
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
let toml_syntax = SyntaxDefinition::load_from_str(TOML_RAW, true, Some("toml")).unwrap();
builder.add(toml_syntax);
let typescript_syntax =
SyntaxDefinition::load_from_str(TYPESCRIPT_RAW, true, Some("typescript")).unwrap();
builder.add(typescript_syntax);
builder.build()
}
lazy_static! {
pub static ref SYNTAX_SET: SyntaxSet = create_syntaxset();
pub static ref THEME_SET: ThemeSet = ThemeSet::load_defaults();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,517 @@
use std::collections::HashMap;
use gray_matter::engine::Engine;
use model::properties::Properties;
use pulldown_cmark::{
escape::{escape_href, escape_html, StrWrite},
Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, Tag,
};
use serde::Deserialize;
use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet};
use crate::parse::properties::{parse_language, parse_language_properties};
use super::highlight::{create_syntaxset, SYNTAX_SET, THEME_SET};
#[derive(Deserialize)]
struct Bookmark {
url: String,
title: String,
description: Option<String>,
author: Option<String>,
publisher: Option<String>,
thumbnail: Option<String>,
icon: Option<String>,
}
fn generate_component_bookmark<W: StrWrite>(mut output: W, source: &str) -> std::io::Result<()> {
let Bookmark {
url,
title,
description,
author,
publisher,
thumbnail,
icon,
} = gray_matter::engine::YAML::parse(source)
.deserialize()
.expect("Bookmark properties");
write!(output, "<figure class=\"w-full text-base\">")?;
write!(output, "<a href=\"")?;
escape_href(&mut output, &url)?;
write!(output, "\" class=\"")?;
write!(
output,
"plain w-full flex flex-col lg:flex-row rounded-md shadow-md min-h-[148px]"
)?;
write!(
output,
"border border-neutral-300 dark:border-neutral-700\">"
)?;
if let Some(thumbnail) = thumbnail {
write!(output, "<div class=\"relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]\">")?;
write!(
output,
"<img class=\"absolute top-0 left-0 w-full h-full rounded-r-md object-cover\" "
)?;
write!(output, "src=\"")?;
escape_href(&mut output, &thumbnail)?;
write!(output, "\" alt=\"")?;
escape_html(&mut output, &title)?;
write!(output, "\" loading=\"lazy\" decoding=\"async\" /></div>")?;
}
write!(
output,
"<div class=\"font-sans lg:order-1 grow flex flex-col justify-start align-start p-5\">"
)?;
write!(output, "<div class=\"font-semibold\">{title}</div>")?;
if let Some(description) = description {
write!(
output,
"<div class=\"grow overflow-y-hidden mt-3 max-h-12\">"
)?;
escape_html(&mut output, &description)?;
write!(output, "</div>")?;
}
write!(
output,
"<div class=\"flex flex-row flex-wrap align-center gap-1 mt-3.5\">"
)?;
if let Some(icon) = icon {
write!(
output,
"<img class=\"w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-2\" alt=\""
)?;
escape_html(&mut output, publisher.as_deref().unwrap_or_default())?;
write!(output, "\" src=\"")?;
escape_href(&mut output, &icon)?;
write!(output, "\" />")?;
}
if let Some(publisher) = publisher {
write!(output, "<span>")?;
escape_html(&mut output, &publisher)?;
write!(output, "</span>")?;
if author.is_some() {
write!(
output,
r#"<svg xmlns="http://www.w3.org/2000/svg" data-license="From https://github.com/twbs/icons - Licensed under MIT" width="24" height="24" fill="currentColor" viewBox="0 0 16 16" class="text-gray-500"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"></path></svg>"#
)?;
}
}
if let Some(author) = author {
write!(output, "<span>")?;
escape_html(&mut output, &author)?;
write!(output, "</span>")?;
}
write!(output, "</div>")?;
write!(output, "</div>")?;
write!(output, "</a>")?;
write!(output, "</figure>")
}
#[derive(Deserialize)]
pub struct Quote {
pub quote: String,
pub author: Option<String>,
pub url: Option<String>,
}
fn generate_component_quote<W: StrWrite>(mut output: W, source: &str) -> std::io::Result<()> {
let Quote { quote, author, url } = gray_matter::engine::YAML::parse(source)
.deserialize()
.expect("Bookmark properties");
write!(output, "<figure class=\"quote\"><p>")?;
escape_html(&mut output, &quote)?;
write!(output, "</p>")?;
if let Some(author) = author {
write!(output, "<cite>")?;
if let Some(url) = url {
write!(output, "<a href=\"")?;
escape_href(&mut output, &url)?;
write!(output, "\" target=\"_blank\" rel=\"noreferrer\">")?;
escape_html(&mut output, &author)?;
write!(output, "</a>")?;
} else {
escape_html(&mut output, &author)?;
}
write!(output, "</cite>")?;
}
write!(output, "</figure>")
}
struct Highlighting {
language: String,
content: String,
}
pub struct Writer<'a, I, W> {
tokens: I,
writer: W,
footnotes: HashMap<CowStr<'a>, usize>,
highlight: Option<Highlighting>,
table_align: Vec<Alignment>,
table_head: bool,
table_colidx: usize,
}
impl<'a, I, W> Writer<'a, I, W>
where
I: Iterator<Item = Event<'a>>,
W: StrWrite,
{
pub fn new(tokens: I, writer: W) -> Self {
Self {
tokens,
writer,
footnotes: HashMap::new(),
highlight: None,
table_align: Vec::new(),
table_head: false,
table_colidx: 0,
}
}
#[inline]
fn write(&mut self, content: &str) -> std::io::Result<()> {
self.writer.write_str(content)
}
fn component(&mut self, name: &str) -> std::io::Result<bool> {
match name {
"bookmark" => {
let content = self.raw_text();
generate_component_bookmark(&mut self.writer, &content)?;
Ok(true)
}
"quote" => {
let content = self.raw_text();
generate_component_quote(&mut self.writer, &content)?;
Ok(true)
}
_ => Ok(false),
}
}
fn start(&mut self, tag: Tag) -> std::io::Result<()> {
match tag {
Tag::Paragraph => self.write("<p>"),
Tag::Heading(level, ident, classes) => {
self.write(match level {
HeadingLevel::H1 => "<h1",
HeadingLevel::H2 => "<h2",
HeadingLevel::H3 => "<h3",
HeadingLevel::H4 => "<h4",
HeadingLevel::H5 => "<h5",
HeadingLevel::H6 => "<h6",
})?;
if let Some(ident) = ident {
self.write(" id=\"")?;
escape_html(&mut self.writer, ident)?;
self.write("\"")?;
}
if !classes.is_empty() {
let classes = classes.join(" ");
self.write(" class=\"")?;
escape_html(&mut self.writer, &classes)?;
self.write("\"")?;
}
self.write(">")
}
Tag::BlockQuote => self.write("<blockquote>"),
Tag::CodeBlock(kind) => {
if let CodeBlockKind::Fenced(language) = &kind {
if self.component(language)? {
return Ok(());
}
}
let language = if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
let language = parse_language(&language);
Some(language)
} else {
None
}
} else {
None
};
self.write("<figure class=\"code\">")?;
self.highlight = None;
if let Some(language) = &language {
if language != "plain" {
self.highlight = Some(Highlighting {
language: language.to_string(),
content: String::new(),
});
}
}
if self.highlight.is_none() {
self.write("<pre><code>")?;
}
Ok(())
}
Tag::List(ordered) => {
if let Some(start) = ordered {
self.write("<ol start=\"")?;
write!(self.writer, "{}", start)?;
self.write("\">")
} else {
self.write("<ul>")
}
}
Tag::Item => self.write("<li>"),
Tag::Table(align) => {
self.table_align = align;
self.write("<table>")
}
Tag::TableHead => {
self.table_head = true;
self.write("<thead><tr>")
}
Tag::TableRow => {
self.table_colidx = 0;
self.write("<tr>")
}
Tag::TableCell => {
if self.table_head {
self.write("<th")?;
} else {
self.write("<td")?;
}
match self.table_align.get(self.table_colidx) {
Some(Alignment::Left) => self.write(" align=\"left\"")?,
Some(Alignment::Center) => self.write(" align=\"center\"")?,
Some(Alignment::Right) => self.write(" align=\"right\"")?,
_ => (),
}
self.write(">")
}
Tag::Emphasis => self.write("<em>"),
Tag::Strong => self.write("<strong>"),
Tag::Strikethrough => self.write("<s>"),
Tag::Link(_, href, title) => {
self.write("<a href=\"")?;
escape_href(&mut self.writer, &href)?;
self.write("\" title=\"")?;
escape_html(&mut self.writer, &title)?;
self.write("\">")
}
Tag::Image(_, href, title) => {
self.write("<figure><img src=\"")?;
escape_href(&mut self.writer, &href)?;
self.write("\" title=\"")?;
escape_html(&mut self.writer, &title)?;
let alt = self.raw_text();
self.write("\" alt=\"")?;
escape_html(&mut self.writer, &alt)?;
self.write("\"><figcaption>")?;
escape_html(&mut self.writer, &alt)?;
self.write("</figcaption></figure>")
}
_ => Ok(()),
}
}
fn end(&mut self, tag: Tag) -> std::io::Result<()> {
match tag {
Tag::Paragraph => self.write("</p>"),
Tag::Heading(level, _, _) => self.write(match level {
HeadingLevel::H1 => "</h1>",
HeadingLevel::H2 => "</h2>",
HeadingLevel::H3 => "</h3>",
HeadingLevel::H4 => "</h4>",
HeadingLevel::H5 => "</h5>",
HeadingLevel::H6 => "</h6>",
}),
Tag::BlockQuote => self.write("</blockquote>"),
Tag::CodeBlock(kind) => {
let mut highlight = None;
std::mem::swap(&mut highlight, &mut self.highlight);
if let Some(Highlighting { language, content }) = highlight {
let syntax = SYNTAX_SET
.find_syntax_by_token(&language)
.unwrap_or_else(|| panic!("Unknown language: {}", language));
let theme = THEME_SET.themes.get("base16-ocean.dark").unwrap();
let html = highlighted_html_for_string(&content, &SYNTAX_SET, syntax, theme)
.expect("syntax highlight");
self.writer.write_str(&html)?;
} else {
self.write("</code></pre>")?;
}
let properties = if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
let (_, properties) = parse_language_properties(&language)
.expect("valid language and properties");
properties
} else {
Properties::default()
}
} else {
Properties::default()
};
if let Some(caption) = properties.get("caption") {
self.write("<figcaption>")?;
escape_html(&mut self.writer, caption)?;
self.write("</figcaption>")?;
}
self.write("</figure>")
}
Tag::List(ordered) => {
if ordered.is_some() {
self.write("</ol>")
} else {
self.write("</ul>")
}
}
Tag::Item => self.write("</li>"),
Tag::Table(_) => self.write("</tbody></table>"),
Tag::TableHead => {
self.table_head = false;
self.write("</tr></thead><tbody>")
}
Tag::TableRow => self.write("</tr>"),
Tag::TableCell => {
if self.table_head {
self.write("</th>")?;
} else {
self.write("</td>")?;
}
self.table_colidx += 1;
Ok(())
}
Tag::Emphasis => self.write("</em>"),
Tag::Strong => self.write("</strong>"),
Tag::Strikethrough => self.write("</s>"),
Tag::Link(_, _, _) => self.write("</a>"),
_ => Ok(()),
}
}
fn raw_text(&mut self) -> String {
let mut output = String::new();
let mut nest = 0;
for event in self.tokens.by_ref() {
match event {
Event::Start(_) => nest += 1,
Event::End(_) => {
if nest == 0 {
break;
}
nest -= 1;
}
Event::Html(text) | Event::Code(text) | Event::Text(text) => output.push_str(&text),
Event::SoftBreak | Event::HardBreak | Event::Rule => output.push(' '),
Event::FootnoteReference(name) => {
let next = self.footnotes.len() + 1;
let footnote = *self.footnotes.entry(name).or_insert(next);
output.push_str(&format!("[{footnote}]"));
}
Event::TaskListMarker(true) => output.push_str("[x]"),
Event::TaskListMarker(false) => output.push_str("[ ]"),
}
}
output
}
pub fn run(mut self) -> std::io::Result<()> {
while let Some(event) = self.tokens.next() {
match event {
Event::Start(tag) => self.start(tag)?,
Event::End(tag) => self.end(tag)?,
Event::Text(text) => {
if let Some(highlight) = &mut self.highlight {
highlight.content.push_str(&text);
} else {
escape_html(&mut self.writer, &text)?;
}
}
Event::Code(text) => {
self.write("<code>")?;
escape_html(&mut self.writer, &text)?;
self.write("</code>")?;
}
Event::Html(html) => self.write(&html)?,
Event::SoftBreak => {
if let Some(highlight) = &mut self.highlight {
highlight.content.push('\n');
} else {
self.write("\n")?;
}
}
Event::HardBreak => {
self.write("<br />")?;
}
_ => {}
}
}
Ok(())
}
}

12
macros/src/error.rs Normal file
View File

@ -0,0 +1,12 @@
use thiserror::Error;
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
pub enum Error {
#[error("Missing front matter in document '{0}'")]
MissingFrontMatter(String),
#[error("IO Error: {0}")]
IoError(#[from] std::io::Error),
#[error("Deserialization error: {0}")]
DeserializationError(#[from] serde_json::Error),
}

21
macros/src/lib.rs Normal file
View File

@ -0,0 +1,21 @@
use documents::DocumentsInput;
use proc_macro::TokenStream;
use syn::parse_macro_input;
use tags::TagsInput;
mod documents;
mod error;
mod parse;
mod tags;
#[proc_macro]
pub fn documents(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DocumentsInput);
documents::generate(input).expect("generate documents")
}
#[proc_macro]
pub fn tags(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as TagsInput);
tags::generate(input).expect("generate tags")
}

2
macros/src/parse.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod frontmatter;
pub mod properties;

View File

@ -0,0 +1,17 @@
use gray_matter::{engine::YAML, Matter, ParsedEntity};
use model::frontmatter::FrontMatter;
pub fn parse_front_matter(
content: &[u8],
) -> Result<(Option<FrontMatter>, ParsedEntity), serde_json::error::Error> {
let content = unsafe { std::str::from_utf8_unchecked(content) };
let matter = Matter::<YAML>::new().parse(content);
let info = if let Some(data) = &matter.data {
Some(data.deserialize()?)
} else {
None
};
Ok((info, matter))
}

View File

@ -0,0 +1,88 @@
use std::collections::HashMap;
use model::properties::Properties;
use thiserror::Error;
use nom::{
branch::alt,
bytes::complete::{is_not, tag},
character::complete::{alpha1, alphanumeric1, multispace0},
combinator::{opt, recognize},
multi::many0_count,
sequence::{delimited, pair, preceded},
IResult,
};
#[derive(Debug, Error)]
pub enum PropertiesParseError {
#[error("Unable to parse identifier: {0}")]
InvalidIdentifier(nom::Err<nom::error::Error<String>>),
#[error("Unable to parse value: {0}")]
InvalidValue(nom::Err<nom::error::Error<String>>),
}
impl PropertiesParseError {
fn invalid_identifier(err: nom::Err<nom::error::Error<&str>>) -> Self {
Self::InvalidIdentifier(err.to_owned())
}
fn invalid_value(err: nom::Err<nom::error::Error<&str>>) -> Self {
Self::InvalidValue(err.to_owned())
}
}
pub fn parse_properties(s: &str) -> Result<Properties, PropertiesParseError> {
let mut input = s;
let mut properties = HashMap::new();
while !input.is_empty() {
let (rest, name): (&str, &str) = preceded(multispace0, identifier)(input)
.map_err(PropertiesParseError::invalid_identifier)?;
let (rest, value) = opt(preceded(tag("="), alt((identifier, string_literal))))(rest)
.map_err(PropertiesParseError::invalid_value)?;
properties.insert(name.to_string(), value.map(ToString::to_string));
input = rest;
}
Ok(Properties::from(properties))
}
fn identifier(input: &str) -> IResult<&str, &str> {
recognize(pair(
alt((alpha1, tag("_"))),
many0_count(alt((alphanumeric1, tag("_")))),
))(input)
}
fn string_literal(input: &str) -> IResult<&str, &str> {
delimited(tag("\""), is_not("\"\\"), tag("\""))(input)
}
pub fn parse_language_properties(
input: &str,
) -> Result<(String, Properties), PropertiesParseError> {
let input = input.trim();
if let Some((language, rest)) = input.split_once(' ') {
let rest = rest.trim();
let properties = if rest.is_empty() {
Properties::default()
} else {
parse_properties(rest)?
};
Ok((language.to_string(), properties))
} else {
Ok((input.to_string(), Properties::default()))
}
}
pub fn parse_language(input: &str) -> String {
let input = input.trim();
if let Some((language, _)) = input.split_once(' ') {
language.to_string()
} else {
input.to_string()
}
}

78
macros/src/tags.rs Normal file
View File

@ -0,0 +1,78 @@
use std::{io::Read, path::PathBuf};
use gray_matter::engine::Engine;
use model::tag::{Tag, TagVisibility};
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
LitStr,
};
use crate::error::Error;
pub struct TagsInput {
pub file: LitStr,
}
impl Parse for TagsInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
file: input.parse()?,
})
}
}
fn generate_tag(
Tag {
slug,
name,
visibility,
description,
}: Tag,
) -> proc_macro2::TokenStream {
let visibility = match visibility {
TagVisibility::Public => quote! { model::tag::TagVisibility::Public },
TagVisibility::Private => quote! { model::tag::TagVisibility::Private },
};
let description = match description {
Some(description) => quote! { Some(#description.to_string()) },
None => quote! { None },
};
quote! {
tags.insert(#slug.to_string(), model::tag::Tag {
slug: #slug.to_string(),
name: #name.to_string(),
visibility: #visibility,
description: #description
});
}
}
pub fn generate(input: TagsInput) -> Result<TokenStream, Error> {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop();
path.push(input.file.value());
let content = {
let mut content = String::new();
let mut file = std::fs::File::open(path)?;
file.read_to_string(&mut content)?;
content
};
let tags = gray_matter::engine::YAML::parse(&content)
.deserialize::<Vec<Tag>>()?
.into_iter()
.map(generate_tag);
Ok(TokenStream::from(quote! {
pub fn tags() -> std::collections::HashMap<String, model::tag::Tag> {
let mut tags = HashMap::new();
#(#tags)*
tags
}
}))
}

1
model/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

123
model/Cargo.lock generated Normal file
View File

@ -0,0 +1,123 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "deranged"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
dependencies = [
"serde",
]
[[package]]
name = "model"
version = "2.0.0"
dependencies = [
"serde",
"thiserror",
"time",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07"
dependencies = [
"deranged",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9"
dependencies = [
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"

11
model/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "model"
version = "2.0.0"
publish = false
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
thiserror = { version = "1.0" }
time = { version = "0.3", features = ["serde", "parsing"] }

62
model/src/document.rs Normal file
View File

@ -0,0 +1,62 @@
use time::OffsetDateTime;
use crate::frontmatter::FrontMatter;
#[derive(Debug, Clone, PartialEq)]
pub struct Summary {
/// The slug used to form the URL for this document.
pub slug: String,
/// The rendered title for the document.
pub title: String,
/// Any given excerpt.
pub excerpt: Option<String>,
/// The date on which this document was published.
pub published: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Details {
/// Document summary.
pub summary: Summary,
/// Tags attached to this post.
pub tags: Vec<String>,
/// The read time (in seconds).
pub reading_time: Option<usize>,
/// The URL to the cover image.
pub cover_image: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
/// Document details
pub details: Details,
/// The main content
pub content: String,
}
impl Details {
pub fn from_front_matter(
slug: String,
reading_time: Option<usize>,
FrontMatter {
title,
tags,
published,
cover,
excerpt,
}: FrontMatter,
) -> Self {
Self {
summary: Summary {
slug,
title,
excerpt,
published,
},
tags,
reading_time,
cover_image: cover,
}
}
}

15
model/src/frontmatter.rs Normal file
View File

@ -0,0 +1,15 @@
use serde::Deserialize;
use time::OffsetDateTime;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct FrontMatter {
pub title: String,
pub tags: Vec<String>,
#[serde(
serialize_with = "time::serde::rfc3339::option::serialize",
deserialize_with = "time::serde::rfc3339::option::deserialize"
)]
pub published: Option<OffsetDateTime>,
pub cover: Option<String>,
pub excerpt: Option<String>,
}

4
model/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod document;
pub mod frontmatter;
pub mod properties;
pub mod tag;

28
model/src/properties.rs Normal file
View File

@ -0,0 +1,28 @@
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Properties {
properties: HashMap<String, Option<String>>,
}
impl From<HashMap<String, Option<String>>> for Properties {
fn from(properties: HashMap<String, Option<String>>) -> Self {
Self { properties }
}
}
impl Properties {
pub fn has(&self, name: &str) -> bool {
self.properties.contains_key(name)
}
pub fn get(&self, name: &str) -> Option<&str> {
if let Some(value) = self.properties.get(name) {
if let Some(value) = value.as_deref() {
return Some(value);
}
}
None
}
}

21
model/src/tag.rs Normal file
View File

@ -0,0 +1,21 @@
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Tag {
/// The slug of the tag
pub slug: String,
/// The display name of the tag
pub name: String,
/// Whether the tag is visible or not
pub visibility: TagVisibility,
/// A description of the tag
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum TagVisibility {
#[serde(rename = "public")]
Public,
#[serde(rename = "private")]
Private,
}

View File

@ -1,4 +1,6 @@
pub mod blog;
// pub mod content;
pub mod content;
pub mod document;
pub mod layout;
pub mod markdown;
// pub mod markdown;

View File

@ -1,13 +1,10 @@
use model::document::Details;
use time::{format_description::FormatItem, macros::format_description};
use yew::{classes, function_component, html, use_context, Html, Properties};
use yew_icons::{Icon, IconId};
use yew_router::prelude::Link;
use crate::{
components::layout::intersperse::Intersperse,
model::{source::TagsContext, PostInfo},
pages::Route,
};
use crate::{components::layout::intersperse::Intersperse, model::TagsContext, pages::Route};
const DATE_FORMAT: &[FormatItem] =
format_description!("[day padding:none] [month repr:short] [year]");
@ -25,11 +22,11 @@ fn post_card_image(slug: &str, image: &Option<String>) -> Html {
}
}
pub fn post_card_details(horizontal: bool, info: &PostInfo, tags: &TagsContext) -> Html {
pub fn post_card_details(horizontal: bool, info: &Details, tags: &TagsContext) -> Html {
let mut facts =
Intersperse::new(html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> });
if let Some(published) = info.doc_info.published {
if let Some(published) = info.summary.published {
facts.push(html! { <div>{published.format(DATE_FORMAT).expect("valid format")}</div> });
}
@ -73,13 +70,13 @@ pub fn post_card_details(horizontal: bool, info: &PostInfo, tags: &TagsContext)
}
}
fn post_card_description(info: &PostInfo, tags: &TagsContext) -> Html {
fn post_card_description(info: &Details, tags: &TagsContext) -> Html {
html! {
<div class="grow flex flex-col gap-4 justify-between">
<Link<Route> classes="unstyled" to={Route::BlogPost { slug: info.doc_info.slug.clone() }}>
<Link<Route> classes="unstyled" to={Route::BlogPost { slug: info.summary.slug.clone() }}>
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{&info.doc_info.title}</h1>
if let Some(excerpt) = &info.doc_info.excerpt {
<h1 class="text-2xl font-bold">{&info.summary.title}</h1>
if let Some(excerpt) = &info.summary.excerpt {
<p class="text-gray-500 font-text text-xl">{excerpt}</p>
}
</div>
@ -92,7 +89,7 @@ fn post_card_description(info: &PostInfo, tags: &TagsContext) -> Html {
#[derive(Properties, PartialEq)]
pub struct PostCardProps {
pub first: bool,
pub post: PostInfo,
pub post: Details,
}
#[function_component(PostCard)]
@ -102,7 +99,7 @@ pub fn post_card(props: &PostCardProps) -> Html {
if props.first {
html! {
<>
{post_card_image(&props.post.doc_info.slug, &props.post.cover_image)}
{post_card_image(&props.post.summary.slug, &props.post.cover_image)}
<div class="xl:col-span-2 md:mt-4 lg:mt-0">
{post_card_description(&props.post, &tags)}
</div>
@ -111,7 +108,7 @@ pub fn post_card(props: &PostCardProps) -> Html {
} else {
html! {
<div class="flex flex-col gap-4 md:mt-20 lg:mt-0">
{post_card_image(&props.post.doc_info.slug, &props.post.cover_image)}
{post_card_image(&props.post.summary.slug, &props.post.cover_image)}
{post_card_description(&props.post, &tags)}
</div>
}

View File

@ -1,10 +1,10 @@
use yew::{function_component, html, use_context, Html};
use crate::{components::blog::post_card::PostCard, model::source::PostsContext};
use crate::{components::blog::post_card::PostCard, model::BlogDetailsContext};
#[function_component(PostCardList)]
pub fn post_card_list() -> Html {
let posts = use_context::<PostsContext>().expect("PostsContext to be provided");
let posts = use_context::<BlogDetailsContext>().expect("BlogDetailsContext to be provided");
html! {
<div class="container mx-auto">

View File

@ -1,18 +1,18 @@
use model::document::Details;
use yew::{function_component, html, use_context, Html, Properties};
use crate::{
components::{blog::post_card::post_card_details, markdown::markdown},
model::{source::TagsContext, Post},
};
use crate::{components::blog::post_card::post_card_details, model::TagsContext};
#[derive(Properties, PartialEq)]
pub struct PostContentProps {}
pub struct PostContentProps {
pub details: Details,
pub content: Html,
}
#[function_component(PostContent)]
pub fn post_content(_: &PostContentProps) -> Html {
pub fn post_content(props: &PostContentProps) -> Html {
let tags = use_context::<TagsContext>().expect("TagsContext to be provided");
let post = use_context::<Post>().expect("Post to be provided");
let style = if let Some(cover_image) = &post.info.cover_image {
let style = if let Some(cover_image) = &props.details.cover_image {
format!(
"background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url({})",
cover_image
@ -26,20 +26,20 @@ pub fn post_content(_: &PostContentProps) -> Html {
<header class="bg-[50%] bg-no-repeat bg-cover bg-fixed pt-20" {style}>
<div class="container mx-auto">
<h1 class="text-5xl font-bold text-center text-white">
{ &post.info.doc_info.title }
{ &props.details.summary.title }
</h1>
if let Some(excerpt) = &post.info.doc_info.excerpt {
if let Some(excerpt) = &props.details.summary.excerpt {
<p class="font-text text-2xl text-white text-center mt-5">
{ excerpt }
</p>
}
<div class="mt-12 pt-8 px-16 bg-white dark:bg-zinc-900 rounded-t">
{post_card_details(true, &post.info, &tags)}
{post_card_details(true, &props.details, &tags)}
</div>
</div>
</header>
<div class="container mx-auto my-12 px-16 markdown">
{markdown(&post.content)}
{props.content.clone()}
</div>
</article>
}

View File

@ -0,0 +1,2 @@
pub mod bookmark;
pub mod quote;

View File

@ -0,0 +1,59 @@
use yew::{function_component, html, Html, Properties};
use yew_icons::{Icon, IconId};
#[derive(Properties, PartialEq)]
pub struct BookmarkProps {
pub url: String,
pub title: String,
pub author: Option<String>,
pub description: Option<String>,
pub publisher: Option<String>,
pub thumbnail: Option<String>,
pub icon: Option<String>,
}
#[function_component(Bookmark)]
pub fn bookmark(props: &BookmarkProps) -> Html {
html! {
<figure class="w-full text-base">
<a href={props.url.clone()}
class="plain w-full flex flex-col lg:flex-row rounded-md shadow-md min-h-[148px] border border-neutral-300 dark:border-neutral-700">
if let Some(thumbnail) = &props.thumbnail {
<div class="relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]">
<img
class="absolute top-0 left-0 w-full h-full rounded-r-md object-cover"
src={thumbnail.clone()}
alt={props.title.clone()}
loading="lazy"
decoding="async" />
</div>
}
<div class="font-sans ld:order-1 grow flex flex-col justify-start align-start p-5">
<div class="font-semibold">{props.title.clone()}</div>
if let Some(description) = &props.description {
<div class="grow overflow-y-hidden mt-3 max-h-12">{description.clone()}</div>
}
<div class="flex flex-row flex-wrap align-center gap-1 mt-3.5">
if let Some(icon) = &props.icon {
<img
class="w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-2"
alt={props.publisher.clone()}
src={icon.clone()} />
}
if let Some(publisher) = &props.publisher {
<span>{publisher}</span>
if props.author.is_some() {
<Icon icon_id={IconId::BootstrapDot} />
}
}
if let Some(author) = &props.author {
<span>{author.clone()}</span>
}
</div>
</div>
</a>
</figure>
}
}

View File

@ -0,0 +1,33 @@
use yew::{function_component, html, Children, Html, Properties};
#[derive(Properties, PartialEq)]
pub struct QuoteProps {
pub author: Option<String>,
pub url: Option<String>,
#[prop_or_default]
pub children: Children,
}
#[function_component(Quote)]
pub fn quote(props: &QuoteProps) -> Html {
let cite = props.author.clone().map(|author| {
props
.url
.clone()
.map(|url| {
html! {
<cite>
<a href={url} target="_blank" rel="noreferrer">{author.clone()}</a>
</cite>
}
})
.unwrap_or_else(|| html! { <cite>{author.clone()}</cite> })
});
html! {
<figure class="quote">
{props.children.clone()}
{cite}
</figure>
}
}

View File

@ -1,87 +1,51 @@
pub mod frontmatter;
pub mod properties;
pub mod source;
// pub mod source;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use std::{collections::HashMap, rc::Rc};
use self::frontmatter::FrontMatter;
use model::{document::Details, tag::Tag};
use yew::{function_component, html, use_memo, Children, ContextProvider, Html, Properties};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DocInfo {
/// The slug used to form the URL for this document.
pub slug: String,
/// The rendered title for the document.
pub title: String,
/// Any given excerpt.
pub excerpt: Option<String>,
/// The date on which this document was published.
pub published: Option<OffsetDateTime>,
macros::tags!("content/tags.yaml");
pub mod blog {
use crate::components::document::bookmark::Bookmark;
macros::documents!("content/blog");
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PostInfo {
/// Document information.
pub doc_info: DocInfo,
/// Tags attached to this post.
pub tags: Vec<String>,
/// The read time (in seconds).
pub reading_time: Option<usize>,
/// The URL to the cover image.
pub cover_image: Option<String>,
#[derive(Properties, PartialEq)]
pub struct ProvideTagsProps {
#[prop_or_default]
pub children: Children,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Post {
/// Information about the post
pub info: PostInfo,
/// The main content
pub content: String,
}
pub type TagsContext = Rc<HashMap<String, Tag>>;
impl PostInfo {
pub fn from_front_matter(
slug: String,
reading_time: Option<usize>,
FrontMatter {
title,
tags,
published,
cover,
excerpt,
}: FrontMatter,
) -> Self {
PostInfo {
doc_info: DocInfo {
slug,
title,
excerpt,
published,
},
#[function_component(ProvideTags)]
pub fn provide_tags(props: &ProvideTagsProps) -> Html {
let tags = use_memo(|_| tags(), 0);
tags,
reading_time,
cover_image: cover,
}
html! {
<ContextProvider<TagsContext> context={tags}>
{props.children.clone()}
</ContextProvider<TagsContext>>
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Tag {
/// The slug of the tag
pub slug: String,
/// The display name of the tag
pub name: String,
/// Whether the tag is visible or not
pub visibility: TagVisibility,
/// A description of the tag
pub description: Option<String>,
#[derive(Properties, PartialEq)]
pub struct ProvideBlogDetailsProps {
#[prop_or_default]
pub children: Children,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TagVisibility {
#[serde(rename = "public")]
Public,
#[serde(rename = "private")]
Private,
pub type BlogDetailsContext = Rc<Vec<Details>>;
#[function_component(ProvideBlogDetails)]
pub fn provide_blog_details(props: &ProvideBlogDetailsProps) -> Html {
let details = use_memo(|_| blog::documents(), 0);
html! {
<ContextProvider<BlogDetailsContext> context={details}>
{props.children.clone()}
</ContextProvider<BlogDetailsContext>>
}
}

View File

@ -1,35 +0,0 @@
use gray_matter::{engine::YAML, Matter, ParsedEntity};
use serde::Deserialize;
use time::OffsetDateTime;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct FrontMatter {
pub title: String,
pub tags: Vec<String>,
#[serde(
serialize_with = "time::serde::rfc3339::option::serialize",
deserialize_with = "time::serde::rfc3339::option::deserialize"
)]
pub published: Option<OffsetDateTime>,
pub cover: Option<String>,
pub excerpt: Option<String>,
}
pub fn parse_front_matter(content: &[u8]) -> (Option<FrontMatter>, ParsedEntity) {
let content = unsafe { std::str::from_utf8_unchecked(content) };
let matter = Matter::<YAML>::new().parse(content);
let info: Option<FrontMatter> = if let Some(data) = &matter.data {
match data.deserialize() {
Ok(info) => Some(info),
Err(err) => {
log::error!("Failed to parse front matter: {err:?}");
None
}
}
} else {
None
};
(info, matter)
}

View File

@ -1,84 +0,0 @@
use std::{collections::HashMap, str::FromStr};
use thiserror::Error;
use nom::{
branch::alt,
bytes::complete::{is_not, tag},
character::complete::{alpha1, alphanumeric1, multispace0},
combinator::{opt, recognize},
multi::many0_count,
sequence::{delimited, pair, preceded},
IResult,
};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Properties {
properties: HashMap<String, Option<String>>,
}
#[derive(Debug, Error)]
pub enum PropertiesParseError {
#[error("Unable to parse identifier: {0}")]
InvalidIdentifier(nom::Err<nom::error::Error<String>>),
#[error("Unable to parse value: {0}")]
InvalidValue(nom::Err<nom::error::Error<String>>),
}
impl PropertiesParseError {
fn invalid_identifier(err: nom::Err<nom::error::Error<&str>>) -> Self {
Self::InvalidIdentifier(err.to_owned())
}
fn invalid_value(err: nom::Err<nom::error::Error<&str>>) -> Self {
Self::InvalidValue(err.to_owned())
}
}
impl FromStr for Properties {
type Err = PropertiesParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut input = s;
let mut properties = HashMap::new();
while !input.is_empty() {
let (rest, name): (&str, &str) = preceded(multispace0, identifier)(input)
.map_err(PropertiesParseError::invalid_identifier)?;
let (rest, value) = opt(preceded(tag("="), alt((identifier, string_literal))))(rest)
.map_err(PropertiesParseError::invalid_value)?;
properties.insert(name.to_string(), value.map(ToString::to_string));
input = rest;
}
Ok(Self { properties })
}
}
fn identifier(input: &str) -> IResult<&str, &str> {
recognize(pair(
alt((alpha1, tag("_"))),
many0_count(alt((alphanumeric1, tag("_")))),
))(input)
}
fn string_literal(input: &str) -> IResult<&str, &str> {
delimited(tag("\""), is_not("\"\\"), tag("\""))(input)
}
impl Properties {
pub fn has(&self, name: &str) -> bool {
self.properties.contains_key(name)
}
pub fn get(&self, name: &str) -> Option<&str> {
if let Some(value) = self.properties.get(name) {
if let Some(value) = value.as_deref() {
return Some(value);
}
}
None
}
}

View File

@ -2,16 +2,16 @@ use yew::{function_component, html, Html};
use crate::{
components::blog::post_card_list::PostCardList,
model::source::{ProvidePosts, ProvideTags},
model::{ProvideBlogDetails, ProvideTags},
};
#[function_component(Page)]
pub fn page() -> Html {
html! {
<ProvideTags>
<ProvidePosts>
<ProvideBlogDetails>
<PostCardList />
</ProvidePosts>
</ProvideBlogDetails>
</ProvideTags>
}
}

View File

@ -1,9 +1,6 @@
use yew::{function_component, html, Html, Properties};
use crate::{
components::content::PostContent,
model::source::{ProvidePost, ProvideTags},
};
use crate::{components::content::PostContent, model::ProvideTags};
#[derive(Properties, PartialEq)]
pub struct PageProps {
@ -12,11 +9,20 @@ pub struct PageProps {
#[function_component(Page)]
pub fn page(props: &PageProps) -> Html {
let Some((details, content)) = crate::model::blog::render(&props.slug) else {
log::error!("Could not find blog post with slug '{}'", &props.slug);
return html! {
<div class="container mx-auto my-12 px-16">
<h1 class="text-5xl font-bold text-center text-white">
{ "Page not found" }
</h1>
</div>
};
};
html! {
<ProvideTags>
<ProvidePost slug={props.slug.clone()}>
<PostContent />
</ProvidePost>
<PostContent details={details} content={content} />
</ProvideTags>
}
}

View File

@ -2,16 +2,16 @@ use yew::{function_component, html, Html};
use crate::{
components::blog::post_card_list::PostCardList,
model::source::{ProvidePosts, ProvideTags},
model::{ProvideBlogDetails, ProvideTags},
};
#[function_component(Page)]
pub fn page() -> Html {
html! {
<ProvideTags>
<ProvidePosts>
<ProvideBlogDetails>
<PostCardList />
</ProvidePosts>
</ProvideBlogDetails>
</ProvideTags>
}
}