Extract CSS and JS from templates and serve as static assets

This commit is contained in:
Magnus Hoff 2017-09-15 14:54:47 +02:00
parent 963085ab9a
commit 53c4ff1b5c
12 changed files with 580 additions and 322 deletions

136
Cargo.lock generated
View file

@ -1,32 +1,11 @@
[root]
name = "sausagewiki"
name = "static_resource_derive"
version = "0.1.0"
dependencies = [
"bart 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"bart_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.26.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_codegen 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"pulldown-cmark 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2-diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -100,6 +79,20 @@ name = "bitflags"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "block-buffer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byte-tools"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.1.0"
@ -188,6 +181,14 @@ dependencies = [
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "digest"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.1"
@ -198,6 +199,11 @@ name = "either"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "futures"
version = "0.1.14"
@ -217,6 +223,15 @@ name = "gcc"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "generic-array"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"nodrop 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "httparse"
version = "1.2.3"
@ -399,6 +414,14 @@ dependencies = [
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "nodrop"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "nom"
version = "2.2.1"
@ -444,6 +467,11 @@ dependencies = [
"libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "odds"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "percent-encoding"
version = "1.0.0"
@ -539,6 +567,38 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sausagewiki"
version = "0.1.0"
dependencies = [
"bart 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"bart_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.26.0 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_codegen 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libsqlite3-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"pulldown-cmark 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2-diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"static_resource_derive 0.1.0",
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.1.0"
@ -603,6 +663,18 @@ dependencies = [
"url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "slab"
version = "0.3.0"
@ -739,6 +811,11 @@ dependencies = [
"futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "typenum"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicase"
version = "2.0.0"
@ -853,6 +930,8 @@ dependencies = [
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum block-buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1339a1042f5d9f295737ad4d9a6ab6bf81c84a933dba110b9200cd6d1448b814"
"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40"
"checksum byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff81738b726f5d099632ceaffe7fb65b90212e8dce59d518729e7e8634032d3d"
"checksum bytes 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d828f97b58cc5de3e40c421d0cf2132d6b2da4ee0e11b8632fa838f0f9333ad6"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
@ -863,11 +942,14 @@ dependencies = [
"checksum diesel 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "67f5b7408ddb7a834c0f191fb49b4398ccae19b226afafeeff5cb1eaac768b89"
"checksum diesel_codegen 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "044abc50a0ee67e195b0ae95c9ffe6903748434294636dd67808301b7df1902f"
"checksum diesel_infer_schema 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cf0e5b3d40abfc2eadba06df2c1b93b61be579390f4dba620f08e8172cce30c4"
"checksum digest 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e5b29bf156f3f4b3c4f610a25ff69370616ae6e0657d416de22645483e72af0a"
"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90"
"checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a"
"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
"checksum futures 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "4b63a4792d4f8f686defe3b39b92127fea6344de5d38202b2ee5a11bbbf29d6a"
"checksum futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a283c84501e92cade5ea673a2a7ca44f71f209ccdd302a3e0896f50083d2c5ff"
"checksum gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)" = "e8310f7e9c890398b0e80e301c4f474e9918d2b27fca8f48486ca775fa9ffc5a"
"checksum generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fceb69994e330afed50c93524be68c42fa898c2d9fd4ee8da03bd7363acd26f2"
"checksum httparse 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "af2f2dd97457e8fb1ae7c5a420db346af389926e36f43768b96f101546b04a07"
"checksum hyper 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "641abc3e3fcf0de41165595f801376e01106bca1fd876dda937730e477ca004c"
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
@ -889,12 +971,14 @@ dependencies = [
"checksum mio 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "dbd91d3bfbceb13897065e97b2ef177a09a438cb33612b2d371bf568819a9313"
"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
"checksum net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "3a80f842784ef6c9a958b68b7516bc7e35883c614004dd94959a4dca1b716c09"
"checksum nodrop 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "52cd74cd09beba596430cc6e3091b74007169a56246e1262f0ba451ea95117b2"
"checksum nom 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
"checksum num 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "a311b77ebdc5dd4cf6449d81e4135d9f0e3b153839ac90e648a8ef538f923525"
"checksum num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "d1452e8b06e448a07f0e6ebb0bb1d92b8890eea63288c0b627331d53514d0fba"
"checksum num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "7485fcc84f85b4ecd0ea527b14189281cf27d60e583ae65ebc9c088b13dffe01"
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
"checksum num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aec53c34f2d0247c5ca5d32cca1478762f301740468ee9ee6dcb7a0dd7a0c584"
"checksum odds 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "c3df9b730298cea3a1c3faa90b7e2f9df3a9c400d0936d6015e6165734eefcba"
"checksum percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de154f638187706bde41d9b4738748933d64e6b37bdbffc0b47a97d16a6ae356"
"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903"
"checksum pulldown-cmark 0.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4c7c04a8bb38f80717527edea39c82378c2ef13ecdbc914cbd90653a2e24afdf"
@ -916,6 +1000,7 @@ dependencies = [
"checksum serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37aee4e0da52d801acfbc0cc219eb1eda7142112339726e427926a6f6ee65d3a"
"checksum serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d243424e06f9f9c39e3cd36147470fd340db785825e367625f79298a6ac6b7ac"
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
"checksum sha2 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d963c78ce367df26d7ea8b8cc655c651b42e8a1e584e869c1e17dae3ccb116a"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
@ -931,6 +1016,7 @@ dependencies = [
"checksum tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ab83e7adb5677e42e405fa4ceff75659d93c4d7d7dd22f52fcec59ee9f02af"
"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389"
"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162"
"checksum typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a99dc6780ef33c78780b826cf9d2a78840b72cae9474de4bcaf9051e60ebbd"
"checksum unicase 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e01da42520092d0cd2d6ac3ae69eb21a22ad43ff195676b86f8c37f487d6b80"
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"

View file

@ -45,6 +45,9 @@ version = "0.1"
version = "0.0.11"
default-features = false
[dependencies.static_resource_derive]
path = "libs/static_resource_derive"
[build-dependencies]
quote = "0.3.10"
walkdir = "1"
@ -53,3 +56,5 @@ walkdir = "1"
default-features = false
features = ["sqlite", "chrono"]
version = "0.15.0"
[workspace]

97
assets/script.js Normal file
View file

@ -0,0 +1,97 @@
function autosizeTextarea(textarea, shadow) {
shadow.style.width = textarea.clientWidth + "px";
shadow.value = textarea.value;
textarea.style.height = shadow.scrollHeight + "px";
}
function queryArgsFromForm(form) {
const items = [];
for (const {name, value} of form.elements) {
if (!name) continue;
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
}
return items.join('&');
}
let hasBeenOpen = false;
function openEditor() {
const article = document.querySelector("article");
const rendered = article.querySelector(".rendered");
const editor = article.querySelector(".editor");
const textarea = editor.querySelector('textarea[name="body"]');
const shadow = editor.querySelector('textarea.shadow-control');
const form = editor.querySelector("form");
const cancel = editor.querySelector('.cancel');
const footer = document.querySelector("footer");
const revision = footer.querySelector(".revision");
const lastUpdated = footer.querySelector(".last-updated");
textarea.style.height = rendered.clientHeight + "px";
article.classList.add('edit');
autosizeTextarea(textarea, shadow);
textarea.focus();
if (hasBeenOpen) return;
hasBeenOpen = true;
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
form.addEventListener("submit", function (ev) {
ev.preventDefault();
ev.stopPropagation();
(async function () {
const body = queryArgsFromForm(form);
textarea.disabled = true;
const response = await fetch(
form.getAttribute("action"),
{
method: 'PUT',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: body,
}
);
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
const result = await response.json();
form.elements.base_revision.value = result.revision;
revision.textContent = result.revision;
lastUpdated.textContent = result.created;
rendered.innerHTML = result.rendered;
article.classList.remove('edit');
textarea.disabled = false;
}()
.catch(err => {
textarea.disabled = false;
console.error(err);
alert(err);
}));
});
cancel.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
article.classList.remove('edit');
form.reset();
});
}
document
.getElementById("openEditor")
.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
openEditor();
})

183
assets/style.css Normal file
View file

@ -0,0 +1,183 @@
html {
font-family: "Apple Garamond", "Baskerville",
"Times New Roman", "Droid Serif", "Times",
"Source Serif Pro", serif;
}
h1 {
font-family: 'Amatic SC', cursive;
font-weight: normal;
font-style: normal;
font-size: 40px;
line-height: 54px;
}
h2 {
font-family: inherit;
font-weight: bold;
font-style: normal;
font-size: 18px;
line-height: 32px;
margin-top: 32px;
margin-bottom: 0;
}
header {
max-width: 600px;
width: 100%;
margin: 40px auto 0 auto;
}
article {
font-size: 18px;
line-height: 32px;
max-width: 600px;
width: 100%;
margin: 0 auto 120px auto;
}
blockquote {
margin-left: 0;
padding-left: 12px;
border-left: 4px solid #eee;
}
p {
margin: 0 0 28px 0;
}
code, pre {
background: #f8f8f8;
font-family: "SF Mono", "Monaco",
"Inconsolata", "Fira Mono",
"Droid Sans Mono", "Source Code Pro",
monospace;
}
pre {
overflow: auto;
}
a {
color: #5e90af;
}
a:visited {
color: inherit;
}
a:hover {
color: #79b9e1;
}
/* Sticky footer */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
article {
flex: 1;
}
footer {
padding: 16px 0;
background: #f8f8f8;
color: #444;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
}
footer dl {
margin: 0;
}
footer dt, footer dd {
display: inline;
margin: 0;
}
footer dt::after {
content: ": ";
display: inline;
}
footer dd::after {
content: "|";
margin: 0 12px;
}
footer dd:last-child::after {
content: "";
margin: 0;
}
textarea {
border: none;
background: none;
margin: 0;
padding: 0;
font-family: "SF Mono", "Monaco",
"Inconsolata", "Fira Mono",
"Droid Sans Mono", "Source Code Pro",
monospace;
width: 100%;
resize: none;
overflow: hidden;
}
.shadow-control {
visibility: hidden;
position: fixed;
height: auto;
min-height: 100px;
}
.editor {
display: none;
}
.editor textarea[name="body"] {
height: 600px;
}
.edit .editor {
display: block;
}
.edit .rendered {
display: none;
}
.editor-controls {
position: fixed;
right: 0;
bottom: 0;
left: 0;
background: #91A238;
padding: 10px 20px;
}
@media (min-width: 600px) {
.editor-controls {
position: fixed;
left: auto;
right: 20px;
bottom: 20px;
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
}
}

View file

@ -0,0 +1,13 @@
[package]
name = "static_resource_derive"
version = "0.1.0"
authors = ["Magnus Hoff <maghoff@gmail.com>"]
[dependencies]
quote = "0.3.10"
syn = "0.10.5"
sha2 = "0.6"
base64 = "0.6"
[lib]
proc-macro = true

View file

@ -0,0 +1,115 @@
#![recursion_limit="128"]
#[macro_use] extern crate quote;
extern crate base64;
extern crate proc_macro;
extern crate sha2;
extern crate syn;
use proc_macro::TokenStream;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
fn user_crate_root() -> PathBuf {
std::env::current_dir().expect("Unable to get current directory")
}
fn find_attr<'a>(attrs: &'a Vec<syn::Attribute>, name: &str) -> Option<&'a str> {
attrs.iter()
.find(|&x| x.name() == name)
.and_then(|ref attr| match &attr.value {
&syn::MetaItem::NameValue(_, syn::Lit::Str(ref template, _)) => Some(template),
_ => None
})
.map(|x| x.as_ref())
}
fn buf_file<P: AsRef<Path>>(filename: P) -> Vec<u8> {
let mut f = File::open(filename)
.expect("Unable to open file for reading");
let mut buf = Vec::new();
f.read_to_end(&mut buf)
.expect("Unable to read file");
buf
}
fn calculate_checksum<P: AsRef<Path>>(filename: P) -> String {
use base64::*;
use sha2::{Sha256, Digest};
encode_config(&Sha256::digest(&buf_file(filename)), URL_SAFE)
}
#[proc_macro_derive(StaticResource, attributes(filename, mime))]
pub fn static_resource(input: TokenStream) -> TokenStream {
let s = input.to_string();
let ast = syn::parse_macro_input(&s).unwrap();
let filename = find_attr(&ast.attrs, "filename")
.expect("The `filename` attribute must be specified");
let abs_filename = user_crate_root().join(filename);
let abs_filename = abs_filename.to_str().expect("Absolute file path must be valid Unicode");
let checksum = calculate_checksum(&abs_filename);
let mime = find_attr(&ast.attrs, "mime")
.expect("The `mime` attribute must be specified");
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let gen = quote! {
#[allow(unused_attributes, unused_qualifications, unknown_lints, clippy)]
#[automatically_derived]
impl #impl_generics Resource for #name #ty_generics #where_clause {
fn allow(&self) -> Vec<::hyper::Method> {
use ::hyper::Method::*;
vec![Options, Head, Get]
}
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
futures::finished(Response::new()
.with_status(::hyper::StatusCode::Ok)
.with_header(::hyper::header::ContentType(
#mime.parse().expect("Statically supplied mime type must be parseable")))
.with_header(::hyper::header::CacheControl(vec![
::hyper::header::CacheDirective::Extension("immutable".to_owned(), None),
::hyper::header::CacheDirective::MaxAge(31556926),
::hyper::header::CacheDirective::Public,
]))
.with_header(::hyper::header::ETag(Self::etag()))
).boxed()
}
fn get(self: Box<Self>) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
let body = include_bytes!(#abs_filename);
self.head().map(move |head|
head
.with_header(::hyper::header::ContentLength(body.len() as u64))
.with_body(body as &'static [u8])
).boxed()
}
fn put(self: Box<Self>, _body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
futures::finished(self.method_not_allowed()).boxed()
}
}
impl #impl_generics #name #ty_generics #where_clause {
fn checksum() -> &'static str {
#checksum
}
fn etag() -> ::hyper::header::EntityTag {
::hyper::header::EntityTag::new(false, Self::checksum().to_owned())
}
}
};
gen.parse().unwrap()
}

View file

@ -3,6 +3,7 @@
#[macro_use] extern crate diesel_codegen;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate static_resource_derive;
extern crate chrono;
extern crate clap;

View file

@ -1,6 +1,7 @@
// #[derive(BartDisplay)] can cause unused extern crates warning:
#![allow(unused_extern_crates)]
use std::collections::HashMap;
use std::fmt;
use futures::{self, Future};
@ -60,6 +61,7 @@ lazy_static! {
struct Layout<'a, T: 'a + fmt::Display> {
pub title: &'a str,
pub body: &'a T,
pub style_css_checksum: &'a str,
}
#[derive(BartDisplay)]
@ -70,12 +72,43 @@ struct NotFound;
#[template = "templates/500.html"]
struct InternalServerError;
#[derive(StaticResource)]
#[filename = "assets/style.css"]
#[mime = "text/css"]
struct StyleCss;
#[derive(StaticResource)]
#[filename = "assets/script.js"]
#[mime = "application/javascript"]
struct ScriptJs;
struct WikiLookup {
state: State,
lookup_map: HashMap<String, Box<Fn() -> Box<Resource + Sync + Send>>>,
}
impl WikiLookup {
fn new(state: State) -> WikiLookup {
let mut lookup_map = HashMap::new();
lookup_map.insert(
format!("/_assets/style-{}.css", StyleCss::checksum()),
Box::new(|| Box::new(StyleCss) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send>>
);
lookup_map.insert(
format!("/_assets/script-{}.js", ScriptJs::checksum()),
Box::new(|| Box::new(ScriptJs) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send>>
);
WikiLookup { state, lookup_map }
}
}
impl Lookup for WikiLookup {
type Resource = ArticleResource;
type Resource = Box<Resource + Send + Sync>;
type Error = Box<::std::error::Error + Send + Sync>;
type Future = futures::BoxFuture<Option<Self::Resource>, Self::Error>;
@ -84,14 +117,17 @@ impl Lookup for WikiLookup {
if path.starts_with("/_") {
// Reserved namespace
return futures::finished(None).boxed();
return futures::finished(
self.lookup_map.get(path).map(|x| x())
).boxed();
}
let slug = &path[1..];
if let Ok(article_id) = slug.parse() {
let state = self.state.clone();
self.state.get_article_revision_by_id(article_id)
.and_then(|x| Ok(x.map(move |article| ArticleResource::new(state, article))))
.and_then(|x| Ok(x.map(move |article| Box::new(ArticleResource::new(state, article)) as Box<Resource + Sync + Send>)))
.boxed()
} else {
futures::finished(None).boxed()
@ -123,7 +159,7 @@ impl Resource for ArticleResource {
).boxed()
}
fn get(self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
fn get(self: Box<Self>) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
use chrono::{self, TimeZone, Local};
#[derive(BartDisplay)]
@ -136,6 +172,8 @@ impl Resource for ArticleResource {
title: &'a str,
raw: &'a str,
rendered: String,
script_js_checksum: &'a str,
}
self.head().map(move |head|
@ -149,12 +187,14 @@ impl Resource for ArticleResource {
title: &self.data.title,
raw: &self.data.body,
rendered: render_markdown(&self.data.body),
}
script_js_checksum: ScriptJs::checksum(),
},
style_css_checksum: StyleCss::checksum(),
}.to_string())
).boxed()
}
fn put(self, body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
fn put(self: Box<Self>, body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
// TODO Check incoming Content-Type
use chrono::{TimeZone, Local};
@ -206,7 +246,7 @@ pub struct Site {
impl Site {
pub fn new(state: State) -> Site {
Site {
root: WikiLookup { state }
root: WikiLookup::new(state)
}
}
@ -216,6 +256,7 @@ impl Site {
.with_body(Layout {
title: "Not found",
body: &NotFound,
style_css_checksum: StyleCss::checksum(),
}.to_string())
.with_status(hyper::StatusCode::NotFound)
}
@ -228,6 +269,7 @@ impl Site {
.with_body(Layout {
title: "Internal server error",
body: &InternalServerError,
style_css_checksum: StyleCss::checksum(),
}.to_string())
.with_status(hyper::StatusCode::InternalServerError)
}

View file

@ -1,9 +1,7 @@
use super::resource;
use futures;
pub trait Lookup {
type Resource: resource::Resource;
type Resource;
type Error;
type Future: futures::Future<Item=Option<Self::Resource>, Error=Self::Error>;

View file

@ -12,8 +12,8 @@ type Error = Box<std::error::Error + Send + Sync>;
pub trait Resource {
fn allow(&self) -> Vec<hyper::Method>;
fn head(&self) -> futures::BoxFuture<server::Response, Error>;
fn get(self) -> futures::BoxFuture<server::Response, Error>;
fn put(self, body: hyper::Body) -> futures::BoxFuture<server::Response, Error>;
fn get(self: Box<Self>) -> futures::BoxFuture<server::Response, Error>;
fn put(self: Box<Self>, body: hyper::Body) -> futures::BoxFuture<server::Response, Error>;
fn options(&self) -> Response {
Response::new()

View file

@ -1,3 +1,5 @@
<script src="_assets/script-{{script_js_checksum}}.js" defer></script>
<header>
<h1>{{title}}</h1>
</header>
@ -32,103 +34,3 @@
<dd class="last-updated">{{created}}</dd>
</dl>
</footer>
<script>
function autosizeTextarea(textarea, shadow) {
shadow.style.width = textarea.clientWidth + "px";
shadow.value = textarea.value;
textarea.style.height = shadow.scrollHeight + "px";
}
function queryArgsFromForm(form) {
const items = [];
for (const {name, value} of form.elements) {
if (!name) continue;
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
}
return items.join('&');
}
let hasBeenOpen = false;
function openEditor() {
const article = document.querySelector("article");
const rendered = article.querySelector(".rendered");
const editor = article.querySelector(".editor");
const textarea = editor.querySelector('textarea[name="body"]');
const shadow = editor.querySelector('textarea.shadow-control');
const form = editor.querySelector("form");
const cancel = editor.querySelector('.cancel');
const footer = document.querySelector("footer");
const revision = footer.querySelector(".revision");
const lastUpdated = footer.querySelector(".last-updated");
textarea.style.height = rendered.clientHeight + "px";
article.classList.add('edit');
autosizeTextarea(textarea, shadow);
textarea.focus();
if (hasBeenOpen) return;
hasBeenOpen = true;
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
form.addEventListener("submit", function (ev) {
ev.preventDefault();
ev.stopPropagation();
(async function () {
const body = queryArgsFromForm(form);
textarea.disabled = true;
const response = await fetch(
form.getAttribute("action"),
{
method: 'PUT',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: body,
}
);
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
const result = await response.json();
form.elements.base_revision.value = result.revision;
revision.textContent = result.revision;
lastUpdated.textContent = result.created;
rendered.innerHTML = result.rendered;
article.classList.remove('edit');
textarea.disabled = false;
}()
.catch(err => {
textarea.disabled = false;
console.error(err);
alert(err);
}));
});
cancel.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
article.classList.remove('edit');
form.reset();
});
}
document
.getElementById("openEditor")
.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
openEditor();
})
</script>

View file

@ -4,191 +4,7 @@
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Amatic+SC" rel="stylesheet">
<style>
html {
font-family: "Apple Garamond", "Baskerville",
"Times New Roman", "Droid Serif", "Times",
"Source Serif Pro", serif;
}
h1 {
font-family: 'Amatic SC', cursive;
font-weight: normal;
font-style: normal;
font-size: 40px;
line-height: 54px;
}
h2 {
font-family: inherit;
font-weight: bold;
font-style: normal;
font-size: 18px;
line-height: 32px;
margin-top: 32px;
margin-bottom: 0;
}
header {
max-width: 600px;
width: 100%;
margin: 40px auto 0 auto;
}
article {
font-size: 18px;
line-height: 32px;
max-width: 600px;
width: 100%;
margin: 0 auto 120px auto;
}
blockquote {
margin-left: 0;
padding-left: 12px;
border-left: 4px solid #eee;
}
p {
margin: 0 0 28px 0;
}
code, pre {
background: #f8f8f8;
font-family: "SF Mono", "Monaco",
"Inconsolata", "Fira Mono",
"Droid Sans Mono", "Source Code Pro",
monospace;
}
pre {
overflow: auto;
}
a {
color: #5e90af;
}
a:visited {
color: inherit;
}
a:hover {
color: #79b9e1;
}
/* Sticky footer */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
article {
flex: 1;
}
footer {
padding: 16px 0;
background: #f8f8f8;
color: #444;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
}
footer dl {
margin: 0;
}
footer dt, footer dd {
display: inline;
margin: 0;
}
footer dt::after {
content: ": ";
display: inline;
}
footer dd::after {
content: "|";
margin: 0 12px;
}
footer dd:last-child::after {
content: "";
margin: 0;
}
textarea {
border: none;
background: none;
margin: 0;
padding: 0;
font-family: "SF Mono", "Monaco",
"Inconsolata", "Fira Mono",
"Droid Sans Mono", "Source Code Pro",
monospace;
width: 100%;
resize: none;
overflow: hidden;
}
.shadow-control {
visibility: hidden;
position: fixed;
height: auto;
min-height: 100px;
}
.editor {
display: none;
}
.editor textarea[name="body"] {
height: 600px;
}
.edit .editor {
display: block;
}
.edit .rendered {
display: none;
}
.editor-controls {
position: fixed;
right: 0;
bottom: 0;
left: 0;
background: #91A238;
padding: 10px 20px;
}
@media (min-width: 600px) {
.editor-controls {
position: fixed;
left: auto;
right: 20px;
bottom: 20px;
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
}
}
</style>
<link href="_assets/style-{{style_css_checksum}}.css" rel="stylesheet">
</head>
<body>
{{{body}}}