Switch over to WebAssembly, Rust and Yew #35
154
Cargo.lock
generated
154
Cargo.lock
generated
@ -156,6 +156,15 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
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]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -214,6 +223,16 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -725,6 +744,15 @@ version = "0.2.147"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
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]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@ -753,6 +781,26 @@ version = "0.4.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -791,6 +839,15 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "model"
|
||||||
|
version = "2.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@ -853,6 +910,28 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
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]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.56"
|
version = "0.10.56"
|
||||||
@ -975,6 +1054,20 @@ version = "0.3.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
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]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.1.25"
|
version = "0.1.25"
|
||||||
@ -1047,6 +1140,15 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.33"
|
||||||
@ -1168,6 +1270,21 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
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]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.22"
|
version = "0.1.22"
|
||||||
@ -1275,11 +1392,10 @@ version = "2.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"gray_matter",
|
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"log",
|
"log",
|
||||||
"nom",
|
"macros",
|
||||||
"pulldown-cmark",
|
"model",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1288,7 +1404,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-logger",
|
"wasm-logger",
|
||||||
"words-count",
|
|
||||||
"yew",
|
"yew",
|
||||||
"yew-hooks",
|
"yew-hooks",
|
||||||
"yew-router",
|
"yew-router",
|
||||||
@ -1352,6 +1467,27 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.8.0"
|
version = "3.8.0"
|
||||||
@ -1620,6 +1756,16 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
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]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -28,20 +28,19 @@ name = "site"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { version = "0.1" }
|
async-trait = { version = "0.1" }
|
||||||
gray_matter = { version = "0.2", default-features = false, features = ["yaml"] }
|
|
||||||
include_dir = { version = "0.7" }
|
include_dir = { version = "0.7" }
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
nom = { version = "7.1" }
|
|
||||||
pulldown-cmark = { version = "0.9" }
|
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
thiserror = { version = "1.0" }
|
thiserror = { version = "1.0" }
|
||||||
words-count = { version = "0.1" }
|
|
||||||
yew = { version = "0.20" }
|
yew = { version = "0.20" }
|
||||||
yew-hooks = { version = "0.2" }
|
yew-hooks = { version = "0.2" }
|
||||||
yew-router = { version = "0.17" }
|
yew-router = { version = "0.17" }
|
||||||
|
|
||||||
|
macros = { path = "./macros" }
|
||||||
|
model = { path = "./model" }
|
||||||
|
|
||||||
[dependencies.time]
|
[dependencies.time]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
|
@ -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.
|
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 {
|
server {
|
||||||
listen 9443 ssl http2;
|
listen 9443 ssl http2;
|
||||||
listen [::]:9443 ssl http2;
|
listen [::]:9443 ssl http2;
|
||||||
|
@ -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:
|
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╶─▶
|
A ◀─╴size╶─▶ B ◀─╴size╶─▶ ◀─╴size╶─▶
|
||||||
┌─────┬───┬───┬─────┬───┬─────┬───┬──────────┬───┬──────────┬─────┬──────────┐
|
┌─────┬───┬───┬─────┬───┬─────┬───┬──────────┬───┬──────────┬─────┬──────────┐
|
||||||
│ C │ H │ H │ … │ H │ … │▒▒▒│ Buffer 0 │░░░│ Buffer 1 │ … │ Buffer n │
|
│ C │ H │ H │ … │ H │ … │▒▒▒│ Buffer 0 │░░░│ Buffer 1 │ … │ Buffer n │
|
||||||
|
@ -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
|
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:
|
at the top of this site:
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
const MastodonLink: FC = () => {
|
const MastodonLink: FC = () => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
1
macros/.gitignore
vendored
Normal file
1
macros/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
531
macros/Cargo.lock
generated
Normal file
531
macros/Cargo.lock
generated
Normal 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
33
macros/Cargo.toml
Normal 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
176
macros/src/documents.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
26
macros/src/documents/highlight.rs
Normal file
26
macros/src/documents/highlight.rs
Normal 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();
|
||||||
|
}
|
3519
macros/src/documents/toml.sublime-syntax
Normal file
3519
macros/src/documents/toml.sublime-syntax
Normal file
File diff suppressed because it is too large
Load Diff
3519
macros/src/documents/typescript.sublime-syntax
Normal file
3519
macros/src/documents/typescript.sublime-syntax
Normal file
File diff suppressed because it is too large
Load Diff
517
macros/src/documents/writer.rs
Normal file
517
macros/src/documents/writer.rs
Normal 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, "e)?;
|
||||||
|
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
12
macros/src/error.rs
Normal 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
21
macros/src/lib.rs
Normal 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
2
macros/src/parse.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod frontmatter;
|
||||||
|
pub mod properties;
|
17
macros/src/parse/frontmatter.rs
Normal file
17
macros/src/parse/frontmatter.rs
Normal 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))
|
||||||
|
}
|
88
macros/src/parse/properties.rs
Normal file
88
macros/src/parse/properties.rs
Normal 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
78
macros/src/tags.rs
Normal 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
1
model/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
123
model/Cargo.lock
generated
Normal file
123
model/Cargo.lock
generated
Normal 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
11
model/Cargo.toml
Normal 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
62
model/src/document.rs
Normal 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
15
model/src/frontmatter.rs
Normal 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
4
model/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod document;
|
||||||
|
pub mod frontmatter;
|
||||||
|
pub mod properties;
|
||||||
|
pub mod tag;
|
28
model/src/properties.rs
Normal file
28
model/src/properties.rs
Normal 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
21
model/src/tag.rs
Normal 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,
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
pub mod blog;
|
pub mod blog;
|
||||||
|
// pub mod content;
|
||||||
pub mod content;
|
pub mod content;
|
||||||
|
pub mod document;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod markdown;
|
// pub mod markdown;
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
|
use model::document::Details;
|
||||||
use time::{format_description::FormatItem, macros::format_description};
|
use time::{format_description::FormatItem, macros::format_description};
|
||||||
use yew::{classes, function_component, html, use_context, Html, Properties};
|
use yew::{classes, function_component, html, use_context, Html, Properties};
|
||||||
use yew_icons::{Icon, IconId};
|
use yew_icons::{Icon, IconId};
|
||||||
use yew_router::prelude::Link;
|
use yew_router::prelude::Link;
|
||||||
|
|
||||||
use crate::{
|
use crate::{components::layout::intersperse::Intersperse, model::TagsContext, pages::Route};
|
||||||
components::layout::intersperse::Intersperse,
|
|
||||||
model::{source::TagsContext, PostInfo},
|
|
||||||
pages::Route,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATE_FORMAT: &[FormatItem] =
|
const DATE_FORMAT: &[FormatItem] =
|
||||||
format_description!("[day padding:none] [month repr:short] [year]");
|
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 =
|
let mut facts =
|
||||||
Intersperse::new(html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> });
|
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> });
|
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! {
|
html! {
|
||||||
<div class="grow flex flex-col gap-4 justify-between">
|
<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">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="text-2xl font-bold">{&info.doc_info.title}</h1>
|
<h1 class="text-2xl font-bold">{&info.summary.title}</h1>
|
||||||
if let Some(excerpt) = &info.doc_info.excerpt {
|
if let Some(excerpt) = &info.summary.excerpt {
|
||||||
<p class="text-gray-500 font-text text-xl">{excerpt}</p>
|
<p class="text-gray-500 font-text text-xl">{excerpt}</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -92,7 +89,7 @@ fn post_card_description(info: &PostInfo, tags: &TagsContext) -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PostCardProps {
|
pub struct PostCardProps {
|
||||||
pub first: bool,
|
pub first: bool,
|
||||||
pub post: PostInfo,
|
pub post: Details,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(PostCard)]
|
#[function_component(PostCard)]
|
||||||
@ -102,7 +99,7 @@ pub fn post_card(props: &PostCardProps) -> Html {
|
|||||||
if props.first {
|
if props.first {
|
||||||
html! {
|
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">
|
<div class="xl:col-span-2 md:mt-4 lg:mt-0">
|
||||||
{post_card_description(&props.post, &tags)}
|
{post_card_description(&props.post, &tags)}
|
||||||
</div>
|
</div>
|
||||||
@ -111,7 +108,7 @@ pub fn post_card(props: &PostCardProps) -> Html {
|
|||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
<div class="flex flex-col gap-4 md:mt-20 lg:mt-0">
|
<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)}
|
{post_card_description(&props.post, &tags)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use yew::{function_component, html, use_context, Html};
|
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)]
|
#[function_component(PostCardList)]
|
||||||
pub fn post_card_list() -> Html {
|
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! {
|
html! {
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
|
use model::document::Details;
|
||||||
use yew::{function_component, html, use_context, Html, Properties};
|
use yew::{function_component, html, use_context, Html, Properties};
|
||||||
|
|
||||||
use crate::{
|
use crate::{components::blog::post_card::post_card_details, model::TagsContext};
|
||||||
components::{blog::post_card::post_card_details, markdown::markdown},
|
|
||||||
model::{source::TagsContext, Post},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PostContentProps {}
|
pub struct PostContentProps {
|
||||||
|
pub details: Details,
|
||||||
|
pub content: Html,
|
||||||
|
}
|
||||||
|
|
||||||
#[function_component(PostContent)]
|
#[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 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) = &props.details.cover_image {
|
||||||
let style = if let Some(cover_image) = &post.info.cover_image {
|
|
||||||
format!(
|
format!(
|
||||||
"background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url({})",
|
"background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url({})",
|
||||||
cover_image
|
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}>
|
<header class="bg-[50%] bg-no-repeat bg-cover bg-fixed pt-20" {style}>
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<h1 class="text-5xl font-bold text-center text-white">
|
<h1 class="text-5xl font-bold text-center text-white">
|
||||||
{ &post.info.doc_info.title }
|
{ &props.details.summary.title }
|
||||||
</h1>
|
</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">
|
<p class="font-text text-2xl text-white text-center mt-5">
|
||||||
{ excerpt }
|
{ excerpt }
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<div class="mt-12 pt-8 px-16 bg-white dark:bg-zinc-900 rounded-t">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="container mx-auto my-12 px-16 markdown">
|
<div class="container mx-auto my-12 px-16 markdown">
|
||||||
{markdown(&post.content)}
|
{props.content.clone()}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
2
src/components/document.rs
Normal file
2
src/components/document.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod bookmark;
|
||||||
|
pub mod quote;
|
59
src/components/document/bookmark.rs
Normal file
59
src/components/document/bookmark.rs
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
33
src/components/document/quote.rs
Normal file
33
src/components/document/quote.rs
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
108
src/model.rs
108
src/model.rs
@ -1,87 +1,51 @@
|
|||||||
pub mod frontmatter;
|
// pub mod source;
|
||||||
pub mod properties;
|
|
||||||
pub mod source;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use std::{collections::HashMap, rc::Rc};
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
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)]
|
macros::tags!("content/tags.yaml");
|
||||||
pub struct DocInfo {
|
|
||||||
/// The slug used to form the URL for this document.
|
pub mod blog {
|
||||||
pub slug: String,
|
use crate::components::document::bookmark::Bookmark;
|
||||||
/// The rendered title for the document.
|
macros::documents!("content/blog");
|
||||||
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, Serialize, Deserialize)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PostInfo {
|
pub struct ProvideTagsProps {
|
||||||
/// Document information.
|
#[prop_or_default]
|
||||||
pub doc_info: DocInfo,
|
pub children: Children,
|
||||||
/// 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, Serialize, Deserialize)]
|
pub type TagsContext = Rc<HashMap<String, Tag>>;
|
||||||
pub struct Post {
|
|
||||||
/// Information about the post
|
|
||||||
pub info: PostInfo,
|
|
||||||
/// The main content
|
|
||||||
pub content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PostInfo {
|
#[function_component(ProvideTags)]
|
||||||
pub fn from_front_matter(
|
pub fn provide_tags(props: &ProvideTagsProps) -> Html {
|
||||||
slug: String,
|
let tags = use_memo(|_| tags(), 0);
|
||||||
reading_time: Option<usize>,
|
|
||||||
FrontMatter {
|
|
||||||
title,
|
|
||||||
tags,
|
|
||||||
published,
|
|
||||||
cover,
|
|
||||||
excerpt,
|
|
||||||
}: FrontMatter,
|
|
||||||
) -> Self {
|
|
||||||
PostInfo {
|
|
||||||
doc_info: DocInfo {
|
|
||||||
slug,
|
|
||||||
title,
|
|
||||||
excerpt,
|
|
||||||
published,
|
|
||||||
},
|
|
||||||
|
|
||||||
tags,
|
html! {
|
||||||
reading_time,
|
<ContextProvider<TagsContext> context={tags}>
|
||||||
cover_image: cover,
|
{props.children.clone()}
|
||||||
}
|
</ContextProvider<TagsContext>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct Tag {
|
pub struct ProvideBlogDetailsProps {
|
||||||
/// The slug of the tag
|
#[prop_or_default]
|
||||||
pub slug: String,
|
pub children: Children,
|
||||||
/// 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, Serialize, Deserialize)]
|
pub type BlogDetailsContext = Rc<Vec<Details>>;
|
||||||
pub enum TagVisibility {
|
|
||||||
#[serde(rename = "public")]
|
#[function_component(ProvideBlogDetails)]
|
||||||
Public,
|
pub fn provide_blog_details(props: &ProvideBlogDetailsProps) -> Html {
|
||||||
#[serde(rename = "private")]
|
let details = use_memo(|_| blog::documents(), 0);
|
||||||
Private,
|
|
||||||
|
html! {
|
||||||
|
<ContextProvider<BlogDetailsContext> context={details}>
|
||||||
|
{props.children.clone()}
|
||||||
|
</ContextProvider<BlogDetailsContext>>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,16 +2,16 @@ use yew::{function_component, html, Html};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::blog::post_card_list::PostCardList,
|
components::blog::post_card_list::PostCardList,
|
||||||
model::source::{ProvidePosts, ProvideTags},
|
model::{ProvideBlogDetails, ProvideTags},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page() -> Html {
|
pub fn page() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<ProvidePosts>
|
<ProvideBlogDetails>
|
||||||
<PostCardList />
|
<PostCardList />
|
||||||
</ProvidePosts>
|
</ProvideBlogDetails>
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use yew::{function_component, html, Html, Properties};
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
|
||||||
use crate::{
|
use crate::{components::content::PostContent, model::ProvideTags};
|
||||||
components::content::PostContent,
|
|
||||||
model::source::{ProvidePost, ProvideTags},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PageProps {
|
pub struct PageProps {
|
||||||
@ -12,11 +9,20 @@ pub struct PageProps {
|
|||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page(props: &PageProps) -> Html {
|
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! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<ProvidePost slug={props.slug.clone()}>
|
<PostContent details={details} content={content} />
|
||||||
<PostContent />
|
|
||||||
</ProvidePost>
|
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,16 @@ use yew::{function_component, html, Html};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::blog::post_card_list::PostCardList,
|
components::blog::post_card_list::PostCardList,
|
||||||
model::source::{ProvidePosts, ProvideTags},
|
model::{ProvideBlogDetails, ProvideTags},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page() -> Html {
|
pub fn page() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<ProvidePosts>
|
<ProvideBlogDetails>
|
||||||
<PostCardList />
|
<PostCardList />
|
||||||
</ProvidePosts>
|
</ProvideBlogDetails>
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user