Compare commits
No commits in common. "master" and "process_markdown_in_sqlite" have entirely different histories.
master
...
process_ma
64 changed files with 2266 additions and 4527 deletions
15
.travis.yml
15
.travis.yml
|
@ -19,6 +19,21 @@ script:
|
||||||
- strip -s target/x86_64-unknown-linux-musl/release/sausagewiki
|
- strip -s target/x86_64-unknown-linux-musl/release/sausagewiki
|
||||||
- XZ_OPT=-9 tar Jcf sausagewiki.tar.xz -C target/x86_64-unknown-linux-musl/release/ sausagewiki
|
- XZ_OPT=-9 tar Jcf sausagewiki.tar.xz -C target/x86_64-unknown-linux-musl/release/ sausagewiki
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: NmM+uk4ijbv5wFF3O7w9KLTrGYbe1mxWAzJDR8cD9rimgORWNQKlHOZtthAQxVgtvmhKAMkzwglgQSX3p0w4yGK5oaV3oO1aA21dzlf0BXL7/BOxgYSTjV+x8O1uIu57ERnf4k2WATCpLSx4r4LpfxMgjdEtIl6LDAnV/zX+HKu7pZAzXvmkS22m5CJbEY4M6DpCAIfpP99dolnyU7h5/AR1njMmzSqGB/naVe5O2j0sBveInsjC+4gPSh9QT/VHZBxbOctcy+kSzwN4iDktkFdYIGe9Z2sDjXsiI39ihXntyOHXA2iVpdkgpIGeLIYBOo+DobgMdS45CzZQ2y9zLnwXwODCgrh8qexxnRpC8RG7uKuVe50R6v4HDPgkjwCJoHicxaEUDiPIsg5qCxEfMYd5qUt21OwEwBN9N8K/RZD0fmgKLE5lQiyxubufeSB4wjpWrXct2M46t25qPFobbZ0kzLCXtZHtKk1mkkk+EWv8UOhRvJ8ih0Fb9ivSOrN6YA1/eRd9/SRntkJriMYmfAW50W3DnyFnPHqdV+x+jHJgcB+DnaDvQnPamk93ZDF/UyUDjVuPJFd0BAFxoRUy6HGaF/yajH4r9g3EdlfSu2IrGDo4vIA9qawBYpHyaSGvYwdCDx4/oUPIAf8sLBS01WOaDJgcmmFey7A/OqSEt6Q=
|
||||||
|
file: sausagewiki.tar.xz
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
repo: maghoff/sausagewiki
|
||||||
|
branch: master
|
||||||
|
rust: stable
|
||||||
|
|
||||||
|
cache: cargo
|
||||||
|
before_cache:
|
||||||
|
- chmod -R a+r $HOME/.cargo
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
except:
|
except:
|
||||||
- "/^untagged-/"
|
- "/^untagged-/"
|
||||||
|
|
|
@ -5,4 +5,3 @@ This is a list of people who have contributed to this project.
|
||||||
- Magnus Hoff (maghoff@gmail.com)
|
- Magnus Hoff (maghoff@gmail.com)
|
||||||
- Johannes Hoff (johanneshoff@gmail.com)
|
- Johannes Hoff (johanneshoff@gmail.com)
|
||||||
- Konstantin Yegupov (kyegupov4@gmail.com)
|
- Konstantin Yegupov (kyegupov4@gmail.com)
|
||||||
- cmal (paul@cmal.info)
|
|
1566
Cargo.lock
generated
1566
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
79
Cargo.toml
79
Cargo.toml
|
@ -1,93 +1,84 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Magnus Hoff <maghoff@gmail.com>"]
|
|
||||||
description = "A wiki engine"
|
|
||||||
license = "GPL-3.0"
|
|
||||||
name = "sausagewiki"
|
name = "sausagewiki"
|
||||||
version = "0.1.0-dev"
|
version = "0.1.0-dev"
|
||||||
edition = "2018"
|
description = "A wiki engine"
|
||||||
|
authors = ["Magnus Hoff <maghoff@gmail.com>"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[profile.release]
|
||||||
quote = "1.0.17"
|
panic = "abort"
|
||||||
walkdir = "1"
|
|
||||||
|
|
||||||
[build-dependencies.diesel]
|
[dev-dependencies]
|
||||||
default-features = false
|
indoc = "0.2"
|
||||||
features = ["sqlite", "chrono"]
|
matches = "0.1"
|
||||||
version = "1.4.8"
|
|
||||||
|
|
||||||
[build-dependencies.diesel_migrations]
|
|
||||||
default-features = false
|
|
||||||
features = ["sqlite"]
|
|
||||||
version = "1.4.0"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bart = "0.1.6"
|
bart = "0.1.4"
|
||||||
bart_derive = "0.1.6"
|
bart_derive = "0.1.4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
clap = "2.31"
|
clap = "2.31"
|
||||||
diff = "0.1"
|
diff = "0.1"
|
||||||
futures = "0.1"
|
futures = "0.1"
|
||||||
futures-cpupool = "0.1"
|
futures-cpupool = "0.1"
|
||||||
hyper = "0.11"
|
hyper = "0.11"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "0.2"
|
||||||
maplit = "1"
|
maplit = "1"
|
||||||
percent-encoding = "1.0"
|
percent-encoding = "1.0"
|
||||||
r2d2 = "0.8"
|
r2d2 = "0.8"
|
||||||
r2d2-diesel = "1.0.0"
|
r2d2-diesel = "1.0.0"
|
||||||
regex = "0.2"
|
regex = "0.2"
|
||||||
seahash = "3.0.5"
|
|
||||||
serde = "1.0.0"
|
serde = "1.0.0"
|
||||||
serde_derive = "1.0.0"
|
serde_derive = "1.0.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.5.3"
|
serde_urlencoded = "0.5"
|
||||||
slug = "0.1"
|
slug = "0.1"
|
||||||
titlecase = "0.10"
|
titlecase = "0.10"
|
||||||
tokio-io = "0.1"
|
tokio-io = "0.1"
|
||||||
tokio-proto = "0.1"
|
tokio-proto = "0.1"
|
||||||
tokio-service = "0.1"
|
tokio-service = "0.1"
|
||||||
serde_plain = "0.3.0"
|
|
||||||
rand = "0.5.5"
|
|
||||||
|
|
||||||
[dependencies.codegen]
|
[dependencies.libsqlite3-sys]
|
||||||
path = "libs/codegen"
|
features = ["bundled"]
|
||||||
|
version = "0.9.1"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["sqlite", "chrono"]
|
features = ["sqlite", "chrono"]
|
||||||
version = "1.4.8"
|
version = "1.3.0"
|
||||||
|
|
||||||
[dependencies.diesel_infer_schema]
|
|
||||||
default-features = false
|
|
||||||
features = ["sqlite"]
|
|
||||||
version = "1.4.0"
|
|
||||||
|
|
||||||
[dependencies.diesel_migrations]
|
[dependencies.diesel_migrations]
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["sqlite"]
|
features = ["sqlite"]
|
||||||
version = "1.4.0"
|
version = "1.3.0"
|
||||||
|
|
||||||
[dependencies.libsqlite3-sys]
|
[dependencies.diesel_infer_schema]
|
||||||
features = ["bundled"]
|
default-features = false
|
||||||
version = "<0.23.0"
|
features = ["sqlite"]
|
||||||
|
version = "1.3.0"
|
||||||
|
|
||||||
[dependencies.num]
|
[dependencies.num]
|
||||||
default-features = false
|
default-features = false
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
|
||||||
[dependencies.pulldown-cmark]
|
[dependencies.pulldown-cmark]
|
||||||
default-features = false
|
|
||||||
git = "https://github.com/maghoff/pulldown-cmark.git"
|
git = "https://github.com/maghoff/pulldown-cmark.git"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dependencies.codegen]
|
||||||
indoc = "1.0.4"
|
path = "libs/codegen"
|
||||||
matches = "0.1"
|
|
||||||
|
|
||||||
[features]
|
[build-dependencies]
|
||||||
dynamic-assets = []
|
quote = "0.3.10"
|
||||||
|
walkdir = "1"
|
||||||
|
|
||||||
[profile]
|
[build-dependencies.diesel]
|
||||||
|
default-features = false
|
||||||
|
features = ["sqlite", "chrono"]
|
||||||
|
version = "1.3.0"
|
||||||
|
|
||||||
[profile.release]
|
[build-dependencies.diesel_migrations]
|
||||||
panic = "abort"
|
default-features = false
|
||||||
|
features = ["sqlite"]
|
||||||
|
version = "1.3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|
|
@ -72,4 +72,3 @@ Command line arguments
|
||||||
Sausagewiki will create an SQLite database file with the filename given in the
|
Sausagewiki will create an SQLite database file with the filename given in the
|
||||||
`DATABASE` parameter and open an HTTP server bound to the configured address,
|
`DATABASE` parameter and open an HTTP server bound to the configured address,
|
||||||
`<address>:<port>`.
|
`<address>:<port>`.
|
||||||
|
|
161
assets/script.js
161
assets/script.js
|
@ -1,5 +1,3 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
function autosizeTextarea(textarea, shadow) {
|
function autosizeTextarea(textarea, shadow) {
|
||||||
shadow.style.width = textarea.clientWidth + "px";
|
shadow.style.width = textarea.clientWidth + "px";
|
||||||
shadow.value = textarea.value;
|
shadow.value = textarea.value;
|
||||||
|
@ -8,17 +6,16 @@ function autosizeTextarea(textarea, shadow) {
|
||||||
|
|
||||||
function queryArgsFromForm(form) {
|
function queryArgsFromForm(form) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (const {name, value, type, checked} of form.elements) {
|
for (const {name, value} of form.elements) {
|
||||||
if (!name) continue;
|
if (!name) continue;
|
||||||
if (type === "radio" && !checked) continue;
|
|
||||||
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
|
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
|
||||||
}
|
}
|
||||||
return items.join('&');
|
return items.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEdited(form) {
|
function isEdited(form) {
|
||||||
for (const {name, value, defaultValue, checked, defaultChecked} of form.elements) {
|
for (const {name, value, defaultValue} of form.elements) {
|
||||||
if (name && ((value !== defaultValue) || (checked !== defaultChecked))) return true;
|
if (name && (value !== defaultValue)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -59,77 +56,40 @@ function confirmDiscard() {
|
||||||
return popup(instantiate("confirm-discard"));
|
return popup(instantiate("confirm-discard"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
let hasBeenOpen = false;
|
||||||
hasBeenOpen: false,
|
|
||||||
saving: false,
|
|
||||||
editing: function () { return document.querySelector(".container").classList.contains('edit'); },
|
|
||||||
hasCancelUrl: function () { return document.querySelector("a.button-cancel").getAttribute('href') !== ""; }
|
|
||||||
};
|
|
||||||
|
|
||||||
function openEditor() {
|
function openEditor() {
|
||||||
const bodyElement = document.querySelector("body");
|
|
||||||
const container = document.querySelector(".container");
|
const container = document.querySelector(".container");
|
||||||
const rendered = container.querySelector(".rendered");
|
const rendered = container.querySelector(".rendered");
|
||||||
const editor = container.querySelector(".editor");
|
const editor = container.querySelector(".editor");
|
||||||
const textarea = editor.querySelector('textarea[name="body"]');
|
const textarea = editor.querySelector('textarea[name="body"]');
|
||||||
const shadow = editor.querySelector('textarea.shadow-control');
|
const shadow = editor.querySelector('textarea.shadow-control');
|
||||||
const form = document.getElementById('article-editor');
|
const form = editor.querySelector("form");
|
||||||
const cancel = form.querySelector('.cancel');
|
const cancel = editor.querySelector('.cancel');
|
||||||
const cancelButton = form.querySelector('button.button-cancel');
|
|
||||||
const cancelInteractionGroup = form.querySelector(".cancel-interaction-group");
|
|
||||||
|
|
||||||
const footer = document.querySelector("footer");
|
const footer = document.querySelector("footer");
|
||||||
const lastUpdated = footer.querySelector(".last-updated");
|
const lastUpdated = footer.querySelector(".last-updated");
|
||||||
|
|
||||||
textarea.style.height = rendered.clientHeight + "px";
|
textarea.style.height = rendered.clientHeight + "px";
|
||||||
|
|
||||||
retainScrollRatio(() => {
|
container.classList.add('edit');
|
||||||
container.classList.add('edit');
|
|
||||||
autosizeTextarea(textarea, shadow);
|
|
||||||
});
|
|
||||||
updateFormEnabledState();
|
|
||||||
|
|
||||||
if (state.hasBeenOpen) return;
|
autosizeTextarea(textarea, shadow);
|
||||||
state.hasBeenOpen = true;
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
if (hasBeenOpen) return;
|
||||||
|
hasBeenOpen = true;
|
||||||
|
|
||||||
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
|
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
|
||||||
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
|
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
|
||||||
|
|
||||||
function updateFormEnabledState() {
|
form.addEventListener("submit", function (ev) {
|
||||||
const baseEnabled = !state.saving && state.editing();
|
ev.preventDefault();
|
||||||
const enabled = {
|
ev.stopPropagation();
|
||||||
cancel: baseEnabled && state.hasCancelUrl(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cancelInteractionGroup.classList.remove(!enabled.cancel ? "interaction-group--root--enabled" : "interaction-group--root--disabled");
|
|
||||||
cancelInteractionGroup.classList.add(enabled.cancel ? "interaction-group--root--enabled" : "interaction-group--root--disabled");
|
|
||||||
|
|
||||||
for (const el of form.elements) {
|
|
||||||
el.disabled = !baseEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelButton.disabled = true;
|
|
||||||
|
|
||||||
// TODO: edit-link in footer?
|
|
||||||
}
|
|
||||||
|
|
||||||
function retainScrollRatio(innerFunction) {
|
|
||||||
const scrollElement = document.body.parentElement;
|
|
||||||
const savedScrollRatio = scrollElement.scrollTop / (scrollElement.scrollHeight - scrollElement.clientHeight);
|
|
||||||
innerFunction();
|
|
||||||
scrollElement.scrollTop = (scrollElement.scrollHeight - scrollElement.clientHeight) * savedScrollRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeEditor() {
|
|
||||||
retainScrollRatio(() => container.classList.remove('edit'));
|
|
||||||
document.activeElement && document.activeElement.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSave() {
|
|
||||||
state.saving = true;
|
|
||||||
updateFormEnabledState();
|
|
||||||
|
|
||||||
const body = queryArgsFromForm(form);
|
const body = queryArgsFromForm(form);
|
||||||
|
textarea.disabled = true;
|
||||||
|
// TODO Disable other interaction as well: title editor, cancel and OK buttons
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
form.getAttribute("action"),
|
form.getAttribute("action"),
|
||||||
|
@ -149,8 +109,7 @@ function openEditor() {
|
||||||
if (probablyLoginRedirect) {
|
if (probablyLoginRedirect) {
|
||||||
return loginDialog(response.url)
|
return loginDialog(response.url)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
state.saving = false;
|
textarea.disabled = false;
|
||||||
updateFormEnabledState();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,10 +117,8 @@ function openEditor() {
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
// Update url-bar, page title, footer and cancel link
|
// Update url-bar, page title and footer
|
||||||
const url = result.slug == "" ? "." : result.slug;
|
window.history.replaceState(null, result.title, result.slug == "" ? "." : result.slug);
|
||||||
window.history.replaceState(null, result.title, url);
|
|
||||||
cancel.setAttribute("href", url);
|
|
||||||
document.querySelector("title").textContent = result.title;
|
document.querySelector("title").textContent = result.title;
|
||||||
lastUpdated.innerHTML = result.last_updated;
|
lastUpdated.innerHTML = result.last_updated;
|
||||||
lastUpdated.classList.remove("missing");
|
lastUpdated.classList.remove("missing");
|
||||||
|
@ -172,22 +129,17 @@ function openEditor() {
|
||||||
form.elements.title.value = result.title;
|
form.elements.title.value = result.title;
|
||||||
shadow.value = textarea.value = result.body;
|
shadow.value = textarea.value = result.body;
|
||||||
|
|
||||||
form.querySelector(`.theme-picker--option[value=${JSON.stringify(result.theme)}]`).checked = true;
|
|
||||||
bodyElement.className = `theme-${result.theme}`;
|
|
||||||
|
|
||||||
// Update form:
|
// Update form:
|
||||||
form.elements.base_revision.value = result.revision;
|
form.elements.base_revision.value = result.revision;
|
||||||
for (const element of form.elements) {
|
for (const element of form.elements) {
|
||||||
element.defaultValue = element.value;
|
element.defaultValue = element.value;
|
||||||
element.defaultChecked = element.checked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.conflict) {
|
if (!result.conflict) {
|
||||||
closeEditor();
|
container.classList.remove('edit');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.saving = false;
|
textarea.disabled = false;
|
||||||
updateFormEnabledState();
|
|
||||||
autosizeTextarea(textarea, shadow);
|
autosizeTextarea(textarea, shadow);
|
||||||
|
|
||||||
if (result.conflict) {
|
if (result.conflict) {
|
||||||
|
@ -197,37 +149,23 @@ function openEditor() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
state.saving = false;
|
textarea.disabled = false;
|
||||||
updateFormEnabledState();
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return alertAsync(err.toString());
|
return alertAsync(err.toString());
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function doCancel() {
|
|
||||||
Promise.resolve(!isEdited(form) || confirmDiscard())
|
|
||||||
.then(doReset => {
|
|
||||||
if (doReset) {
|
|
||||||
closeEditor();
|
|
||||||
updateFormEnabledState();
|
|
||||||
form.reset();
|
|
||||||
|
|
||||||
let selectedTheme = form.querySelector(`.theme-picker--option[checked]`).value;
|
|
||||||
bodyElement.className = `theme-${selectedTheme}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addEventListener("submit", function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
doSave();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cancel.addEventListener('click', function (ev) {
|
cancel.addEventListener('click', function (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
doCancel();
|
|
||||||
|
Promise.resolve(!isEdited(form) || confirmDiscard())
|
||||||
|
.then(doReset => {
|
||||||
|
if (doReset) {
|
||||||
|
container.classList.remove('edit');
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("beforeunload", function (ev) {
|
window.addEventListener("beforeunload", function (ev) {
|
||||||
|
@ -236,43 +174,8 @@ function openEditor() {
|
||||||
return ev.returnValue = "Discard changes?";
|
return ev.returnValue = "Discard changes?";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keypress", function (ev) {
|
|
||||||
const accel = ev.ctrlKey || ev.metaKey; // Imprecise, but works cross platform
|
|
||||||
if (ev.key === "Enter" && accel) {
|
|
||||||
if (!state.editing()) return;
|
|
||||||
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
doSave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const themeOptions = form.querySelectorAll(".theme-picker--option");
|
|
||||||
for (let themeOption of themeOptions) {
|
|
||||||
themeOption.addEventListener("click", function (ev) {
|
|
||||||
bodyElement.className = `theme-${ev.target.value}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeTheme() {
|
|
||||||
const form = document.getElementById('article-editor');
|
|
||||||
|
|
||||||
let preSelectedTheme = form.querySelector(`.theme-picker--option[checked]`);
|
|
||||||
if (preSelectedTheme) return;
|
|
||||||
|
|
||||||
let themes = form.querySelectorAll(`.theme-picker--option`);
|
|
||||||
let randomThemeId = (Math.random() * themes.length) | 0;
|
|
||||||
|
|
||||||
let theme = themes[randomThemeId];
|
|
||||||
theme.defaultChecked = theme.checked = true;
|
|
||||||
document.querySelector("body").className = `theme-${theme.value}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeTheme();
|
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("openEditor")
|
.getElementById("openEditor")
|
||||||
.addEventListener("click", function (ev) {
|
.addEventListener("click", function (ev) {
|
||||||
|
|
316
assets/style.css
316
assets/style.css
|
@ -1,9 +1,13 @@
|
||||||
.prototype {
|
@font-face {
|
||||||
display: none;
|
font-family: 'Amatic SC';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Amatic SC Regular'), local('AmaticSC-Regular'),
|
||||||
|
url('amatic-sc-v9-latin-regular.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
.prototype {
|
||||||
margin: 0; /* reset for Safari */
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -13,6 +17,8 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
font-family: 'Amatic SC', sans-serif;
|
||||||
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|
||||||
|
@ -58,16 +64,15 @@ h1+*, h2+*, h3+*, h4+*, h5+*, h6+* {
|
||||||
|
|
||||||
article>hr {
|
article>hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 6px solid var(--theme-main);
|
border-top: 1px solid black;
|
||||||
width: 40px;
|
max-width: 400px;
|
||||||
|
width: 70%;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
background: var(--theme-main);
|
background: lightyellow;
|
||||||
color: var(--theme-text);
|
padding: 16px 48px;
|
||||||
|
|
||||||
padding: 1px 24px;
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
|
||||||
|
@ -76,22 +81,6 @@ article>hr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
}
|
}
|
||||||
.notice a {
|
|
||||||
color: var(--theme-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
background: var(--theme-main);
|
|
||||||
color: var(--theme-text);
|
|
||||||
|
|
||||||
/* Hack to force containing the children instead of collapsing marigins */
|
|
||||||
border: 1px solid var(--theme-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
background: var(--theme-main);
|
|
||||||
color: var(--theme-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
header, article>*, .search {
|
header, article>*, .search {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -108,7 +97,7 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
margin: 50px auto 120px auto;
|
margin: 0 auto 120px auto;
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
@ -159,19 +148,7 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #1976D2;
|
color: #457796;
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href^="http"]::after {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 0.1rem;
|
|
||||||
font-size: 75%;
|
|
||||||
content: "🔗";
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href^="http"]:hover::after {
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,10 +182,10 @@ body {
|
||||||
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
padding: 16px 8px 16px 8px;
|
padding: 0 8px;
|
||||||
|
|
||||||
background: var(--theme-main);
|
background: #f8f8f8;
|
||||||
color: var(--theme-text);
|
color: #444;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: -apple-system, BlinkMacSystemFont,
|
font-family: -apple-system, BlinkMacSystemFont,
|
||||||
"Segoe UI", "Roboto", "Oxygen",
|
"Segoe UI", "Roboto", "Oxygen",
|
||||||
|
@ -216,10 +193,6 @@ footer {
|
||||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
|
||||||
color: var(--theme-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.dense {
|
ul.dense {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -275,11 +248,6 @@ h1>input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero input {
|
|
||||||
background: var(--theme-input);
|
|
||||||
color: var(--theme-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-control {
|
.shadow-control {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -309,163 +277,18 @@ h1>input {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
box-sizing: border-box;
|
background: #91A238;
|
||||||
|
padding: 10px 20px;
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
box-shadow: 0px 5px 20px rgba(0,0,0, 0.2);
|
|
||||||
|
|
||||||
background: white;
|
|
||||||
color: var(--theme-text);
|
|
||||||
padding: 10px 10px;
|
|
||||||
|
|
||||||
transform: translate(0, 65px);
|
|
||||||
transition: transform 100ms;
|
|
||||||
transition-timing-function: linear;
|
|
||||||
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit .editor-controls {
|
@media (min-width: 630px) {
|
||||||
transform: translate(0, 0);
|
|
||||||
transition-timing-function: cubic-bezier(.17,.84,.44,1);
|
|
||||||
|
|
||||||
pointer-events: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-picker {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-picker--option {
|
|
||||||
/* reset */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
-o-appearance: none;
|
|
||||||
-ms-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
height: 20px;
|
|
||||||
background: var(--theme-main);
|
|
||||||
color: var(--theme-text);
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-picker--option:checked::after {
|
|
||||||
content: " ";
|
|
||||||
display: block;
|
|
||||||
background: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: calc(50% - 5px);
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
width: 120px;
|
|
||||||
text-align: center;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont,
|
|
||||||
"Segoe UI", "Roboto", "Oxygen",
|
|
||||||
"Ubuntu", "Cantarell", "Fira Sans",
|
|
||||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
|
|
||||||
padding: 10px 0px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.button[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.button:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.button:not([disabled]):hover, .button:not([disabled]):active {
|
|
||||||
background: var(--button-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-cancel {
|
|
||||||
background: white;
|
|
||||||
color: var(--theme-main);
|
|
||||||
--button-alt: #f0f0f0;
|
|
||||||
}
|
|
||||||
.button-default {
|
|
||||||
background: var(--theme-main);
|
|
||||||
color: var(--theme-text);
|
|
||||||
--button-alt: var(--theme-input);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-interaction-group {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interaction-group--root--enabled .interaction-group--disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interaction-group--root--disabled .interaction-group--enabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
|
||||||
/* min-width is calculated like this:
|
|
||||||
|
|
||||||
body-width = width of the main text column
|
|
||||||
controls-width = width of .editor-controls element, including drop-shadow
|
|
||||||
|
|
||||||
min-width = body-width + 2*controls-width = 600 + 2 * 180 = 960
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
.editor-controls {
|
.editor-controls {
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: calc(50% + 320px);
|
left: auto;
|
||||||
width: 140px;
|
right: 20px;
|
||||||
top: calc(50% - 55px);
|
bottom: 20px;
|
||||||
height: 110px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
transform: translate(20px, 0);
|
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
transition: transform 100ms, opacity 100ms;
|
|
||||||
transition-timing-function: linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit .editor-controls {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
transition-timing-function: cubic-bezier(.17,.84,.44,1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,18 +313,18 @@ article ul.search-results {
|
||||||
display: block;
|
display: block;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 8px 16px;
|
border: 1px solid #ccc;
|
||||||
background: white;
|
padding: 8px;
|
||||||
color: black;
|
|
||||||
}
|
}
|
||||||
.search-result a:hover, .search-result a:focus {
|
.search-result a:hover, .search-result a:focus {
|
||||||
background: var(--theme-main);
|
background: #0074D9;
|
||||||
color: var(--theme-text);
|
border-color: #0074D9;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 45px;
|
margin-top: 30px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,18 +333,16 @@ input[type="search"]::-webkit-search-decoration {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"] {
|
input[type="search"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: textfield;
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
transition: max-width 200ms;
|
||||||
|
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: 1px solid #ccc;
|
||||||
background: var(--theme-input);
|
|
||||||
color: var(--theme-text);
|
|
||||||
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
@ -529,12 +350,13 @@ input[type="search"] {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="search"]::placeholder, .hero input::placeholder {
|
.search input:focus, .search.focus input {
|
||||||
color: var(--theme-text);
|
max-width: 300px;
|
||||||
opacity: 0.6;
|
border-color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search .live-results {
|
.search .live-results {
|
||||||
|
@ -542,8 +364,9 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 266px; /* 300px - padding - border */
|
||||||
|
|
||||||
|
background: white;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@ -553,15 +376,6 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-widget-container {
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-results.show {
|
.live-results.show {
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
@ -569,8 +383,13 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
.live-results .search-result {
|
.live-results .search-result {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.live-results a {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
.live-results .search-result.error {
|
.live-results .search-result.error {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
@ -578,24 +397,16 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
@media (min-width: 630px) {
|
@media (min-width: 630px) {
|
||||||
.search {
|
.search {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
height: 38px;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-widget-container {
|
.search input {
|
||||||
position: absolute;
|
max-width: 125px;
|
||||||
right: 8px;
|
|
||||||
width: 300px;
|
|
||||||
box-shadow: 0 0 0 rgba(0,0,0,0.2);
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.focus .search-widget-container {
|
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search .live-results {
|
.search .live-results {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
margin: 0 16px;
|
||||||
max-height: 0px;
|
max-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,7 +447,7 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
box-shadow: 0px 5px 20px rgba(0,0,0, 0.2);
|
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup>.message {
|
.popup>.message {
|
||||||
|
@ -688,14 +499,6 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
|
||||||
background: none;
|
|
||||||
color: initial;
|
|
||||||
|
|
||||||
/* Disable hack to force containing the children instead of collapsing marigins */
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
/* This doesn't work at all, but it might start to! */
|
/* This doesn't work at all, but it might start to! */
|
||||||
break-after: avoid;
|
break-after: avoid;
|
||||||
|
@ -708,23 +511,10 @@ input[type="search"]::placeholder, .hero input::placeholder {
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href^="http"]::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
article {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
article>hr {
|
|
||||||
border-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 22pt;
|
|
||||||
line-height: 33pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
article, h2, h3, h4, h5, h6, .notice {
|
article, h2, h3, h4, h5, h6, .notice {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
line-height: 18pt;
|
line-height: 18pt;
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Test themes – Sausagewiki</title>
|
|
||||||
<link href="themes.css" rel="stylesheet">
|
|
||||||
<link href="style.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.themed {
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--theme-main);
|
|
||||||
color: var(--theme-text);
|
|
||||||
}
|
|
||||||
.link {
|
|
||||||
color: var(--theme-link);
|
|
||||||
}
|
|
||||||
.proto {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#bar {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#bar>div {
|
|
||||||
height: 30px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="bar"></div>
|
|
||||||
<div class="proto">
|
|
||||||
<div class="themed">The <span class="link">quick</span> brown <span class="link">dog</span> jumps over the lazy log <span class="theme-name"></span></div>
|
|
||||||
<div class="themed"><input type=search placeholder=placeholder> <input type=search value="Bacon"></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const themes = ["red", "pink", "purple", "deep-purple", "indigo", "blue", "light-blue", "cyan", "teal", "green", "light-green", "lime", "yellow", "amber", "orange", "deep-orange", "brown", "gray", "blue-gray"];
|
|
||||||
const body = document.querySelector("body");
|
|
||||||
const proto = document.querySelector(".proto");
|
|
||||||
for (theme of themes) {
|
|
||||||
const block = proto.cloneNode(true);
|
|
||||||
block.className = `theme-${theme}`;
|
|
||||||
block.querySelector(".theme-name").textContent = theme;
|
|
||||||
body.appendChild(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bar = document.querySelector("#bar");
|
|
||||||
for (theme of themes) {
|
|
||||||
const block = document.createElement("div");
|
|
||||||
block.className = `theme-${theme} themed`;
|
|
||||||
bar.appendChild(block);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,134 +0,0 @@
|
||||||
.theme-red {
|
|
||||||
--theme-main: #F44336;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #E57373;
|
|
||||||
--theme-link: #FFF59D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-pink {
|
|
||||||
--theme-main: #E91E63;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #F06292;
|
|
||||||
--theme-link: #FFF59D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-purple {
|
|
||||||
--theme-main: #9C27B0;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #BA68C8;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-deep-purple {
|
|
||||||
--theme-main: #673AB7;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #9575CD;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-indigo {
|
|
||||||
--theme-main: #3F51B5;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #7986CB;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-blue {
|
|
||||||
--theme-main: #2196F3;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #64B5F6;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light-blue {
|
|
||||||
--theme-main: #03A9F4;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #4FC3F7;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-cyan {
|
|
||||||
--theme-main: #00ACC1;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #26C6DA;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-teal {
|
|
||||||
--theme-main: #009688;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #4DB6AC;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-green {
|
|
||||||
--theme-main: #4CAF50;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #81C784;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-light-green {
|
|
||||||
--theme-main: #7CB342;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #9CCC65;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-lime {
|
|
||||||
--theme-main: #C0CA33;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #AFB42B;
|
|
||||||
--theme-link: #1976D2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-yellow {
|
|
||||||
--theme-main: #FDD835;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #FBC02D;
|
|
||||||
--theme-link: #1976D2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-amber {
|
|
||||||
--theme-main: #FFB300;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #FFA000;
|
|
||||||
--theme-link: #1976D2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-orange {
|
|
||||||
--theme-main: #FB8C00;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #FFA726;
|
|
||||||
--theme-link: #FFF59D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-deep-orange {
|
|
||||||
--theme-main: #FF5722;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #FF8A65;
|
|
||||||
--theme-link: #FFF59D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-brown {
|
|
||||||
--theme-main: #795548;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #A1887F;
|
|
||||||
--theme-link: #FFF59D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-gray {
|
|
||||||
--theme-main: #9E9E9E;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #E0E0E0;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-blue-gray {
|
|
||||||
--theme-main: #607D8B;
|
|
||||||
--theme-text: white;
|
|
||||||
--theme-input: #90A4AE;
|
|
||||||
--theme-link: #90CAF9;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
61
build.rs
61
build.rs
|
@ -1,9 +1,10 @@
|
||||||
#[macro_use]
|
#[macro_use] extern crate quote;
|
||||||
extern crate diesel;
|
#[macro_use] extern crate diesel;
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
extern crate walkdir;
|
||||||
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use quote::quote;
|
use diesel::prelude::*;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
@ -14,64 +15,52 @@ use walkdir::WalkDir;
|
||||||
mod sqlfunc {
|
mod sqlfunc {
|
||||||
use diesel::sql_types::Text;
|
use diesel::sql_types::Text;
|
||||||
sql_function!(fn markdown_to_fts(text: Text) -> Text);
|
sql_function!(fn markdown_to_fts(text: Text) -> Text);
|
||||||
sql_function!(fn theme_from_str_hash(text: Text) -> Text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR");
|
let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR");
|
||||||
let db_path = Path::new(&out_dir).join("build.db");
|
let db_path = Path::new(&out_dir).join("build.db");
|
||||||
let db_path = db_path
|
let db_path = db_path.to_str().expect("Will only work for Unicode-representable paths");
|
||||||
.to_str()
|
|
||||||
.expect("Will only work for Unicode-representable paths");
|
|
||||||
|
|
||||||
let _ignore_failure = std::fs::remove_file(db_path);
|
let _ignore_failure = std::fs::remove_file(db_path);
|
||||||
|
|
||||||
let connection = SqliteConnection::establish(db_path)
|
let connection = SqliteConnection::establish(db_path)
|
||||||
.unwrap_or_else(|_| panic!("Error esablishing a database connection to {}", db_path));
|
.expect(&format!("Error esablishing a database connection to {}", db_path));
|
||||||
|
|
||||||
// Integer is a dummy placeholder. Compiling fails when passing ().
|
// Integer is a dummy placeholder. Compiling fails when passing ().
|
||||||
diesel::expression::sql_literal::sql::<diesel::sql_types::Integer>("PRAGMA foreign_keys = ON")
|
diesel::expression::sql_literal::sql::<(diesel::sql_types::Integer)>("PRAGMA foreign_keys = ON")
|
||||||
.execute(&connection)
|
.execute(&connection)
|
||||||
.expect("Should be able to enable foreign keys");
|
.expect("Should be able to enable foreign keys");
|
||||||
|
|
||||||
sqlfunc::markdown_to_fts::register_impl(&connection, |_: String| -> String { unreachable!() })
|
sqlfunc::markdown_to_fts::register_impl(&connection, |_: String| -> String { unreachable!() }).unwrap();
|
||||||
.unwrap();
|
|
||||||
sqlfunc::theme_from_str_hash::register_impl(&connection, |_: String| -> String {
|
|
||||||
unreachable!()
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
diesel_migrations::run_pending_migrations(&connection).unwrap();
|
diesel_migrations::run_pending_migrations(&connection).unwrap();
|
||||||
|
|
||||||
let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs");
|
let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs");
|
||||||
let mut file = File::create(infer_schema_path).expect("Unable to open file for writing");
|
let mut file = File::create(infer_schema_path).expect("Unable to open file for writing");
|
||||||
|
|
||||||
file.write_all(
|
file.write_all(quote! {
|
||||||
quote! {
|
mod __diesel_infer_schema_articles {
|
||||||
mod __diesel_infer_schema_articles {
|
infer_table_from_schema!(#db_path, "articles");
|
||||||
infer_table_from_schema!(#db_path, "articles");
|
|
||||||
}
|
|
||||||
pub use self::__diesel_infer_schema_articles::*;
|
|
||||||
|
|
||||||
mod __diesel_infer_schema_article_revisions {
|
|
||||||
infer_table_from_schema!(#db_path, "article_revisions");
|
|
||||||
}
|
|
||||||
pub use self::__diesel_infer_schema_article_revisions::*;
|
|
||||||
}
|
}
|
||||||
.to_string()
|
pub use self::__diesel_infer_schema_articles::*;
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.expect("Unable to write to file");
|
|
||||||
|
|
||||||
for entry in WalkDir::new("migrations")
|
mod __diesel_infer_schema_article_revisions {
|
||||||
.into_iter()
|
infer_table_from_schema!(#db_path, "article_revisions");
|
||||||
.filter_map(|e| e.ok())
|
}
|
||||||
{
|
pub use self::__diesel_infer_schema_article_revisions::*;
|
||||||
|
}.as_str().as_bytes()).expect("Unable to write to file");
|
||||||
|
|
||||||
|
for entry in WalkDir::new("migrations").into_iter().filter_map(|e| e.ok()) {
|
||||||
println!("cargo:rerun-if-changed={}", entry.path().display());
|
println!("cargo:rerun-if-changed={}", entry.path().display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// For build_config.rs
|
// For build_config.rs
|
||||||
for env_var in &["CONTINUOUS_INTEGRATION", "TRAVIS_BRANCH", "TRAVIS_COMMIT"] {
|
for env_var in &[
|
||||||
|
"CONTINUOUS_INTEGRATION",
|
||||||
|
"TRAVIS_BRANCH",
|
||||||
|
"TRAVIS_COMMIT",
|
||||||
|
] {
|
||||||
println!("cargo:rerun-if-env-changed={}", env_var);
|
println!("cargo:rerun-if-env-changed={}", env_var);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
#![recursion_limit = "128"]
|
#![recursion_limit="128"]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use] extern crate quote;
|
||||||
extern crate quote;
|
#[macro_use] extern crate serde_derive;
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
extern crate serde;
|
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
extern crate serde;
|
||||||
extern crate sha2;
|
extern crate sha2;
|
||||||
extern crate syn;
|
extern crate syn;
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,13 @@ use std::fs::File;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote;
|
use quote;
|
||||||
use serde::de::IgnoredAny;
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
use serde::de::IgnoredAny;
|
||||||
|
|
||||||
const SOURCES: &[&str] = &["src/licenses/license-hound.json", "src/licenses/other.json"];
|
const SOURCES: &[&str] = &[
|
||||||
|
"src/licenses/license-hound.json",
|
||||||
|
"src/licenses/other.json",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||||
pub enum LicenseId {
|
pub enum LicenseId {
|
||||||
|
@ -19,7 +22,7 @@ impl LicenseId {
|
||||||
fn include_notice(&self) -> bool {
|
fn include_notice(&self) -> bool {
|
||||||
use self::LicenseId::*;
|
use self::LicenseId::*;
|
||||||
match self {
|
match self {
|
||||||
Mpl2 => false,
|
&Mpl2 => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +32,10 @@ impl quote::ToTokens for LicenseId {
|
||||||
fn to_tokens(&self, tokens: &mut quote::Tokens) {
|
fn to_tokens(&self, tokens: &mut quote::Tokens) {
|
||||||
use self::LicenseId::*;
|
use self::LicenseId::*;
|
||||||
tokens.append(match self {
|
tokens.append(match self {
|
||||||
Bsd3Clause => "Bsd3Clause",
|
&Bsd3Clause => "Bsd3Clause",
|
||||||
Mit => "Mit",
|
&Mit => "Mit",
|
||||||
Mpl2 => "Mpl2",
|
&Mpl2 => "Mpl2",
|
||||||
Ofl11 => "Ofl11",
|
&Ofl11 => "Ofl11",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,16 +56,12 @@ struct LicenseReport {
|
||||||
impl quote::ToTokens for LicenseReport {
|
impl quote::ToTokens for LicenseReport {
|
||||||
fn to_tokens(&self, tokens: &mut quote::Tokens) {
|
fn to_tokens(&self, tokens: &mut quote::Tokens) {
|
||||||
let c: &LicenseDescription = self.conclusion.as_ref().unwrap();
|
let c: &LicenseDescription = self.conclusion.as_ref().unwrap();
|
||||||
let (name, link, copyright, license) = (
|
let (name, link, copyright, license) =
|
||||||
&self.package_name,
|
(&self.package_name, &c.link, &c.copyright_notice, &c.chosen_license);
|
||||||
&c.link,
|
|
||||||
&c.copyright_notice,
|
|
||||||
&c.chosen_license,
|
|
||||||
);
|
|
||||||
|
|
||||||
let link = match *link {
|
let link = match link {
|
||||||
Some(ref link) => quote! { Some(#link) },
|
&Some(ref link) => quote! { Some(#link) },
|
||||||
None => quote! { None },
|
&None => quote! { None },
|
||||||
};
|
};
|
||||||
|
|
||||||
let copyright = match license.include_notice() {
|
let copyright = match license.include_notice() {
|
||||||
|
@ -86,10 +85,7 @@ pub fn licenses(_input: TokenStream) -> TokenStream {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| -> Vec<LicenseReport> { serde_json::from_reader(File::open(x).unwrap()).unwrap() })
|
.map(|x| -> Vec<LicenseReport> { serde_json::from_reader(File::open(x).unwrap()).unwrap() })
|
||||||
.map(|x| x.into_iter().filter(|x| x.conclusion.is_ok()))
|
.map(|x| x.into_iter().filter(|x| x.conclusion.is_ok()))
|
||||||
.fold(vec![], |mut a, b| {
|
.fold(vec![], |mut a, b| { a.extend(b); a });
|
||||||
a.extend(b);
|
|
||||||
a
|
|
||||||
});
|
|
||||||
|
|
||||||
license_infos.sort_unstable_by_key(|x| x.package_name.to_lowercase());
|
license_infos.sort_unstable_by_key(|x| x.package_name.to_lowercase());
|
||||||
|
|
||||||
|
|
|
@ -10,29 +10,30 @@ fn user_crate_root() -> PathBuf {
|
||||||
std::env::current_dir().expect("Unable to get current directory")
|
std::env::current_dir().expect("Unable to get current directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_attr<'a>(attrs: &'a [syn::Attribute], name: &str) -> Option<&'a str> {
|
fn find_attr<'a>(attrs: &'a Vec<syn::Attribute>, name: &str) -> Option<&'a str> {
|
||||||
attrs
|
attrs.iter()
|
||||||
.iter()
|
|
||||||
.find(|&x| x.name() == name)
|
.find(|&x| x.name() == name)
|
||||||
.and_then(|attr| match attr.value {
|
.and_then(|ref attr| match &attr.value {
|
||||||
syn::MetaItem::NameValue(_, syn::Lit::Str(ref template, _)) => Some(template),
|
&syn::MetaItem::NameValue(_, syn::Lit::Str(ref template, _)) => Some(template),
|
||||||
_ => None,
|
_ => None
|
||||||
})
|
})
|
||||||
.map(|x| x.as_ref())
|
.map(|x| x.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn buf_file<P: AsRef<Path>>(filename: P) -> Vec<u8> {
|
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 f = File::open(filename)
|
||||||
|
.expect("Unable to open file for reading");
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
f.read_to_end(&mut buf).expect("Unable to read file");
|
f.read_to_end(&mut buf)
|
||||||
|
.expect("Unable to read file");
|
||||||
|
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_checksum<P: AsRef<Path>>(filename: P) -> String {
|
fn calculate_checksum<P: AsRef<Path>>(filename: P) -> String {
|
||||||
use base64::*;
|
use base64::*;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
encode_config(&Sha256::digest(&buf_file(filename)), URL_SAFE)
|
encode_config(&Sha256::digest(&buf_file(filename)), URL_SAFE)
|
||||||
}
|
}
|
||||||
|
@ -41,24 +42,15 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
|
||||||
let s = input.to_string();
|
let s = input.to_string();
|
||||||
let ast = syn::parse_macro_input(&s).unwrap();
|
let ast = syn::parse_macro_input(&s).unwrap();
|
||||||
|
|
||||||
let filename =
|
let filename = find_attr(&ast.attrs, "filename")
|
||||||
find_attr(&ast.attrs, "filename").expect("The `filename` attribute must be specified");
|
.expect("The `filename` attribute must be specified");
|
||||||
let abs_filename = user_crate_root().join(filename);
|
let abs_filename = user_crate_root().join(filename);
|
||||||
let abs_filename = abs_filename
|
let abs_filename = abs_filename.to_str().expect("Absolute file path must be valid Unicode");
|
||||||
.to_str()
|
|
||||||
.expect("Absolute file path must be valid Unicode");
|
|
||||||
|
|
||||||
let checksum = calculate_checksum(&abs_filename);
|
let checksum = calculate_checksum(&abs_filename);
|
||||||
|
|
||||||
let path: &Path = filename.as_ref();
|
let mime = find_attr(&ast.attrs, "mime")
|
||||||
let resource_name = format!(
|
.expect("The `mime` attribute must be specified");
|
||||||
"{}-{}.{}",
|
|
||||||
path.file_stem().unwrap().to_str().unwrap(),
|
|
||||||
checksum,
|
|
||||||
path.extension().unwrap().to_str().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let mime = find_attr(&ast.attrs, "mime").expect("The `mime` attribute must be specified");
|
|
||||||
|
|
||||||
let name = &ast.ident;
|
let name = &ast.ident;
|
||||||
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
||||||
|
@ -98,12 +90,12 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics #name #ty_generics #where_clause {
|
impl #impl_generics #name #ty_generics #where_clause {
|
||||||
pub fn resource_name() -> &'static str {
|
pub fn checksum() -> &'static str {
|
||||||
#resource_name
|
#checksum
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn etag() -> ::hyper::header::EntityTag {
|
pub fn etag() -> ::hyper::header::EntityTag {
|
||||||
::hyper::header::EntityTag::new(false, #checksum.to_owned())
|
::hyper::header::EntityTag::new(false, Self::checksum().to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
ALTER TABLE article_revisions ADD COLUMN theme TEXT NOT NULL CHECK (theme IN (
|
|
||||||
'red', 'pink', 'purple', 'deep-purple', 'indigo', 'blue', 'light-blue',
|
|
||||||
'cyan', 'teal', 'green', 'light-green', 'lime', 'yellow', 'amber',
|
|
||||||
'orange', 'deep-orange', 'brown', 'gray', 'blue-gray'
|
|
||||||
)) DEFAULT 'red';
|
|
||||||
|
|
||||||
UPDATE article_revisions SET theme=theme_from_str_hash(title);
|
|
106
src/assets.rs
106
src/assets.rs
|
@ -1,90 +1,24 @@
|
||||||
#[cfg(not(feature = "dynamic-assets"))]
|
use futures::Future;
|
||||||
mod static_assets {
|
use web::{Resource, ResponseFuture};
|
||||||
use crate::web::{Resource, ResponseFuture};
|
|
||||||
use futures::Future;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// The CSS should be built to a single CSS file at compile time
|
#[derive(StaticResource)]
|
||||||
#[derive(StaticResource)]
|
#[filename = "assets/style.css"]
|
||||||
#[filename = "assets/themes.css"]
|
#[mime = "text/css"]
|
||||||
#[mime = "text/css"]
|
pub struct StyleCss;
|
||||||
pub struct ThemesCss;
|
|
||||||
|
|
||||||
#[derive(StaticResource)]
|
#[derive(StaticResource)]
|
||||||
#[filename = "assets/style.css"]
|
#[filename = "assets/script.js"]
|
||||||
#[mime = "text/css"]
|
#[mime = "application/javascript"]
|
||||||
pub struct StyleCss;
|
pub struct ScriptJs;
|
||||||
|
|
||||||
#[derive(StaticResource)]
|
#[derive(StaticResource)]
|
||||||
#[filename = "assets/script.js"]
|
#[filename = "assets/search.js"]
|
||||||
#[mime = "application/javascript"]
|
#[mime = "application/javascript"]
|
||||||
pub struct ScriptJs;
|
pub struct SearchJs;
|
||||||
|
|
||||||
#[derive(StaticResource)]
|
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
|
||||||
#[filename = "assets/search.js"]
|
// Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com)
|
||||||
#[mime = "application/javascript"]
|
#[derive(StaticResource)]
|
||||||
pub struct SearchJs;
|
#[filename = "assets/amatic-sc-v9-latin-regular.woff"]
|
||||||
|
#[mime = "application/font-woff"]
|
||||||
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
|
pub struct AmaticFont;
|
||||||
// Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com)
|
|
||||||
// #[derive(StaticResource)]
|
|
||||||
// #[filename = "assets/amatic-sc-v9-latin-regular.woff"]
|
|
||||||
// #[mime = "application/font-woff"]
|
|
||||||
// pub struct AmaticFont;
|
|
||||||
|
|
||||||
type BoxResource = Box<dyn Resource + Sync + Send>;
|
|
||||||
type ResourceFn = Box<dyn Fn() -> BoxResource + Sync + Send>;
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref ASSETS_MAP: HashMap<&'static str, ResourceFn> = hashmap!{
|
|
||||||
// The CSS should be built to a single CSS file at compile time
|
|
||||||
ThemesCss::resource_name() =>
|
|
||||||
Box::new(|| Box::new(ThemesCss) as BoxResource) as ResourceFn,
|
|
||||||
|
|
||||||
StyleCss::resource_name() =>
|
|
||||||
Box::new(|| Box::new(StyleCss) as BoxResource) as ResourceFn,
|
|
||||||
|
|
||||||
ScriptJs::resource_name() =>
|
|
||||||
Box::new(|| Box::new(ScriptJs) as BoxResource) as ResourceFn,
|
|
||||||
|
|
||||||
SearchJs::resource_name() =>
|
|
||||||
Box::new(|| Box::new(SearchJs) as BoxResource) as ResourceFn,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "dynamic-assets"))]
|
|
||||||
pub use self::static_assets::*;
|
|
||||||
|
|
||||||
#[cfg(feature = "dynamic-assets")]
|
|
||||||
mod dynamic_assets {
|
|
||||||
pub struct ThemesCss;
|
|
||||||
impl ThemesCss {
|
|
||||||
pub fn resource_name() -> &'static str {
|
|
||||||
"themes.css"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StyleCss;
|
|
||||||
impl StyleCss {
|
|
||||||
pub fn resource_name() -> &'static str {
|
|
||||||
"style.css"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ScriptJs;
|
|
||||||
impl ScriptJs {
|
|
||||||
pub fn resource_name() -> &'static str {
|
|
||||||
"script.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SearchJs;
|
|
||||||
impl SearchJs {
|
|
||||||
pub fn resource_name() -> &'static str {
|
|
||||||
"search.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "dynamic-assets")]
|
|
||||||
pub use self::dynamic_assets::*;
|
|
||||||
|
|
|
@ -7,12 +7,9 @@ pub const PROJECT_NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
|
|
||||||
const SOFT_HYPHEN: &str = "\u{00AD}";
|
const SOFT_HYPHEN: &str = "\u{00AD}";
|
||||||
|
|
||||||
#[cfg(all(not(debug_assertions), feature = "dynamic-assets"))]
|
|
||||||
compile_error!("dynamic-assets must not be used for production");
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref VERSION: String = || -> String {
|
pub static ref VERSION: String = || -> String {
|
||||||
let mut components = vec![];
|
let mut components = Vec::<String>::new();
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
components.push("debug".into());
|
components.push("debug".into());
|
||||||
|
@ -20,10 +17,7 @@ lazy_static! {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
components.push("test".into());
|
components.push("test".into());
|
||||||
|
|
||||||
#[cfg(feature = "dynamic-assets")]
|
if let None = option_env!("CONTINUOUS_INTEGRATION") {
|
||||||
components.push("dynamic-assets".into());
|
|
||||||
|
|
||||||
if option_env!("CONTINUOUS_INTEGRATION").is_none() {
|
|
||||||
components.push("local-build".into());
|
components.push("local-build".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,22 +26,26 @@ lazy_static! {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(commit) = option_env!("TRAVIS_COMMIT") {
|
if let Some(commit) = option_env!("TRAVIS_COMMIT") {
|
||||||
components.push(format!(
|
components.push(format!("commit:{}",
|
||||||
"commit:{}",
|
|
||||||
commit
|
commit
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.chunks(4)
|
.chunks(4)
|
||||||
.map(|x| String::from_utf8(x.to_owned()).unwrap_or_else(|_| String::new()))
|
.map(|x|
|
||||||
|
String::from_utf8(x.to_owned())
|
||||||
|
.unwrap_or_else(|_| String::new())
|
||||||
|
)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(SOFT_HYPHEN)
|
.join(SOFT_HYPHEN)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !components.is_empty() {
|
if components.len() > 0 {
|
||||||
format!("{} ({})", env!("CARGO_PKG_VERSION"), components.join(" "))
|
format!("{} ({})", env!("CARGO_PKG_VERSION"), components.join(" "))
|
||||||
} else {
|
} else {
|
||||||
env!("CARGO_PKG_VERSION").to_string()
|
env!("CARGO_PKG_VERSION").to_string()
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
pub static ref HTTP_SERVER: String = format!("{}/{}", PROJECT_NAME, VERSION.as_str());
|
|
||||||
|
pub static ref HTTP_SERVER: String =
|
||||||
|
format!("{}/{}", PROJECT_NAME, VERSION.as_str());
|
||||||
}
|
}
|
||||||
|
|
74
src/db.rs
74
src/db.rs
|
@ -1,11 +1,10 @@
|
||||||
use diesel::expression::sql_literal::sql;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use diesel::expression::sql_literal::sql;
|
||||||
use diesel::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
use r2d2::{CustomizeConnection, Pool};
|
use r2d2::{CustomizeConnection, Pool};
|
||||||
use r2d2_diesel::{self, ConnectionManager};
|
use r2d2_diesel::{self, ConnectionManager};
|
||||||
|
|
||||||
use crate::rendering;
|
use rendering;
|
||||||
use crate::theme;
|
|
||||||
|
|
||||||
embed_migrations!();
|
embed_migrations!();
|
||||||
|
|
||||||
|
@ -13,35 +12,27 @@ embed_migrations!();
|
||||||
struct SqliteInitializer;
|
struct SqliteInitializer;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod sqlfunc {
|
mod sqlfunc {
|
||||||
use diesel::sql_types::Text;
|
use diesel::sql_types::Text;
|
||||||
sql_function!(fn markdown_to_fts(text: Text) -> Text);
|
sql_function!(fn markdown_to_fts(text: Text) -> Text);
|
||||||
sql_function!(fn theme_from_str_hash(text: Text) -> Text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer {
|
impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer {
|
||||||
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> {
|
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> {
|
||||||
sql::<Integer>("PRAGMA foreign_keys = ON")
|
sql::<(Integer)>("PRAGMA foreign_keys = ON")
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map_err(r2d2_diesel::Error::QueryError)?;
|
.map_err(|x| r2d2_diesel::Error::QueryError(x))?;
|
||||||
|
|
||||||
sqlfunc::markdown_to_fts::register_impl(conn, |text: String| {
|
sqlfunc::markdown_to_fts::register_impl(
|
||||||
rendering::render_markdown_for_fts(&text)
|
conn,
|
||||||
})
|
|text: String| rendering::render_markdown_for_fts(&text)
|
||||||
.map_err(r2d2_diesel::Error::QueryError)?;
|
).map_err(|x| r2d2_diesel::Error::QueryError(x))?;
|
||||||
|
|
||||||
sqlfunc::theme_from_str_hash::register_impl(conn, |title: String| {
|
|
||||||
theme::theme_from_str_hash(&title)
|
|
||||||
})
|
|
||||||
.map_err(r2d2_diesel::Error::QueryError)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_pool<S: Into<String>>(
|
pub fn create_pool<S: Into<String>>(connection_string: S) -> Result<Pool<ConnectionManager<SqliteConnection>>, Box<::std::error::Error>> {
|
||||||
connection_string: S,
|
|
||||||
) -> Result<Pool<ConnectionManager<SqliteConnection>>, Box<dyn (::std::error::Error)>> {
|
|
||||||
let manager = ConnectionManager::<SqliteConnection>::new(connection_string);
|
let manager = ConnectionManager::<SqliteConnection>::new(connection_string);
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
.connection_customizer(Box::new(SqliteInitializer {}))
|
.connection_customizer(Box::new(SqliteInitializer {}))
|
||||||
|
@ -62,48 +53,3 @@ pub fn test_connection() -> SqliteConnection {
|
||||||
|
|
||||||
conn
|
conn
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use diesel::sql_query;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn markdown_to_fts() {
|
|
||||||
let conn = test_connection();
|
|
||||||
|
|
||||||
#[derive(QueryableByName, PartialEq, Eq, Debug)]
|
|
||||||
struct Row {
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = sql_query("SELECT markdown_to_fts('[link](url)') as text")
|
|
||||||
.load::<Row>(&conn)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected = rendering::render_markdown_for_fts("[link](url)");
|
|
||||||
|
|
||||||
assert_eq!(expected, res[0].text);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn theme_from_str_hash() {
|
|
||||||
let conn = test_connection();
|
|
||||||
|
|
||||||
#[derive(QueryableByName, PartialEq, Eq, Debug)]
|
|
||||||
struct Row {
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
theme: theme::Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = sql_query("SELECT theme_from_str_hash('Bartefjes') as theme")
|
|
||||||
.load::<Row>(&conn)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected = theme::theme_from_str_hash("Bartefjes");
|
|
||||||
|
|
||||||
assert_eq!(expected, res[0].theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
74
src/lib.rs
74
src/lib.rs
|
@ -1,33 +1,31 @@
|
||||||
#![allow(clippy::into_iter_on_ref)]
|
#![recursion_limit="128"] // for diesel's infer_schema!
|
||||||
#![allow(clippy::vec_init_then_push)]
|
|
||||||
#![recursion_limit = "128"]
|
|
||||||
// for diesel's infer_schema!
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)] #[macro_use] extern crate matches;
|
||||||
#[macro_use]
|
#[cfg(test)] #[macro_use] extern crate indoc;
|
||||||
extern crate matches;
|
|
||||||
#[macro_use]
|
#[macro_use] extern crate bart_derive;
|
||||||
extern crate bart_derive;
|
#[macro_use] extern crate codegen;
|
||||||
#[macro_use]
|
#[macro_use] #[allow(deprecated)] extern crate diesel_infer_schema;
|
||||||
extern crate codegen;
|
#[macro_use] extern crate diesel_migrations;
|
||||||
#[macro_use]
|
#[macro_use] extern crate diesel;
|
||||||
#[allow(clippy::useless_attribute)]
|
#[macro_use] extern crate hyper;
|
||||||
#[allow(deprecated)]
|
#[macro_use] extern crate lazy_static;
|
||||||
extern crate diesel_infer_schema;
|
#[macro_use] extern crate maplit;
|
||||||
#[macro_use]
|
#[macro_use] extern crate serde_derive;
|
||||||
extern crate diesel_migrations;
|
|
||||||
#[macro_use]
|
extern crate chrono;
|
||||||
extern crate diesel;
|
extern crate diff;
|
||||||
#[macro_use]
|
extern crate futures_cpupool;
|
||||||
extern crate hyper;
|
extern crate futures;
|
||||||
#[macro_use]
|
extern crate percent_encoding;
|
||||||
extern crate lazy_static;
|
extern crate pulldown_cmark;
|
||||||
#[macro_use]
|
extern crate r2d2_diesel;
|
||||||
extern crate maplit;
|
extern crate r2d2;
|
||||||
#[macro_use]
|
extern crate serde_json;
|
||||||
extern crate serde_derive;
|
extern crate serde_urlencoded;
|
||||||
#[macro_use]
|
extern crate serde;
|
||||||
extern crate serde_plain;
|
extern crate slug;
|
||||||
|
extern crate titlecase;
|
||||||
|
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
|
@ -42,26 +40,22 @@ mod resources;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod site;
|
mod site;
|
||||||
mod state;
|
mod state;
|
||||||
mod theme;
|
|
||||||
mod web;
|
mod web;
|
||||||
mod wiki_lookup;
|
mod wiki_lookup;
|
||||||
|
|
||||||
pub fn main(
|
pub fn main(db_file: String, bind_host: IpAddr, bind_port: u16, trust_identity: bool) -> Result<(), Box<std::error::Error>> {
|
||||||
db_file: String,
|
|
||||||
bind_host: IpAddr,
|
|
||||||
bind_port: u16,
|
|
||||||
trust_identity: bool,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let db_pool = db::create_pool(db_file)?;
|
let db_pool = db::create_pool(db_file)?;
|
||||||
let cpu_pool = futures_cpupool::CpuPool::new_num_cpus();
|
let cpu_pool = futures_cpupool::CpuPool::new_num_cpus();
|
||||||
|
|
||||||
let state = state::State::new(db_pool, cpu_pool);
|
let state = state::State::new(db_pool, cpu_pool);
|
||||||
let lookup = wiki_lookup::WikiLookup::new(state, trust_identity);
|
let lookup = wiki_lookup::WikiLookup::new(state, trust_identity);
|
||||||
|
|
||||||
let server = hyper::server::Http::new()
|
let server =
|
||||||
.bind(&SocketAddr::new(bind_host, bind_port), move || {
|
hyper::server::Http::new()
|
||||||
Ok(site::Site::new(lookup.clone(), trust_identity))
|
.bind(
|
||||||
})?;
|
&SocketAddr::new(bind_host, bind_port),
|
||||||
|
move || Ok(site::Site::new(lookup.clone(), trust_identity))
|
||||||
|
)?;
|
||||||
|
|
||||||
println!("Listening on http://{}", server.local_addr().unwrap());
|
println!("Listening on http://{}", server.local_addr().unwrap());
|
||||||
|
|
||||||
|
|
86
src/main.rs
86
src/main.rs
|
@ -1,10 +1,11 @@
|
||||||
#[macro_use]
|
#[macro_use] extern crate lazy_static;
|
||||||
extern crate lazy_static;
|
extern crate clap;
|
||||||
|
extern crate sausagewiki;
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
mod build_config;
|
mod build_config;
|
||||||
use crate::build_config::*;
|
use build_config::*;
|
||||||
|
|
||||||
const DATABASE: &str = "DATABASE";
|
const DATABASE: &str = "DATABASE";
|
||||||
const TRUST_IDENTITY: &str = "trust-identity";
|
const TRUST_IDENTITY: &str = "trust-identity";
|
||||||
|
@ -17,61 +18,52 @@ fn args<'a>() -> clap::ArgMatches<'a> {
|
||||||
App::new(PROJECT_NAME)
|
App::new(PROJECT_NAME)
|
||||||
.version(VERSION.as_str())
|
.version(VERSION.as_str())
|
||||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||||
.arg(
|
.arg(Arg::with_name(DATABASE)
|
||||||
Arg::with_name(DATABASE)
|
.help("Sets the database file to use")
|
||||||
.help("Sets the database file to use")
|
.required(true))
|
||||||
.required(true),
|
.arg(Arg::with_name(PORT)
|
||||||
)
|
.help("Sets the listening port")
|
||||||
.arg(
|
.short("p")
|
||||||
Arg::with_name(PORT)
|
.long(PORT)
|
||||||
.help("Sets the listening port")
|
.default_value("8080")
|
||||||
.short("p")
|
.validator(|x| match x.parse::<u16>() {
|
||||||
.long(PORT)
|
Ok(_) => Ok(()),
|
||||||
.default_value("8080")
|
Err(_) => Err("Must be an integer in the range [0, 65535]".into())
|
||||||
.validator(|x| match x.parse::<u16>() {
|
})
|
||||||
Ok(_) => Ok(()),
|
.takes_value(true))
|
||||||
Err(_) => Err("Must be an integer in the range [0, 65535]".into()),
|
.arg(Arg::with_name(ADDRESS)
|
||||||
})
|
.help("Sets the IP address to bind to")
|
||||||
.takes_value(true),
|
.short("a")
|
||||||
)
|
.long(ADDRESS)
|
||||||
.arg(
|
.default_value("127.0.0.1")
|
||||||
Arg::with_name(ADDRESS)
|
.validator(|x| match x.parse::<IpAddr>() {
|
||||||
.help("Sets the IP address to bind to")
|
Ok(_) => Ok(()),
|
||||||
.short("a")
|
Err(_) => Err("Must be a valid IP address".into())
|
||||||
.long(ADDRESS)
|
})
|
||||||
.default_value("127.0.0.1")
|
.takes_value(true))
|
||||||
.validator(|x| match x.parse::<IpAddr>() {
|
.arg(Arg::with_name(TRUST_IDENTITY)
|
||||||
Ok(_) => Ok(()),
|
.help("Trust the value in the X-Identity header to be an \
|
||||||
Err(_) => Err("Must be a valid IP address".into()),
|
|
||||||
})
|
|
||||||
.takes_value(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name(TRUST_IDENTITY)
|
|
||||||
.help(
|
|
||||||
"Trust the value in the X-Identity header to be an \
|
|
||||||
authenticated username. This only makes sense when Sausagewiki \
|
authenticated username. This only makes sense when Sausagewiki \
|
||||||
runs behind a reverse proxy which sets this header.",
|
runs behind a reverse proxy which sets this header.")
|
||||||
)
|
.long(TRUST_IDENTITY))
|
||||||
.long(TRUST_IDENTITY),
|
|
||||||
)
|
|
||||||
.get_matches()
|
.get_matches()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<std::error::Error>> {
|
||||||
let args = args();
|
let args = args();
|
||||||
|
|
||||||
const CLAP: &str = "Guaranteed by clap";
|
const CLAP: &str = "Guaranteed by clap";
|
||||||
const VALIDATOR: &str = "Guaranteed by clap validator";
|
const VALIDATOR: &str = "Guaranteed by clap validator";
|
||||||
let db_file = args.value_of(DATABASE).expect(CLAP).to_owned();
|
let db_file = args.value_of(DATABASE).expect(CLAP).to_owned();
|
||||||
let bind_host = args
|
let bind_host = args.value_of(ADDRESS).expect(CLAP).parse().expect(VALIDATOR);
|
||||||
.value_of(ADDRESS)
|
|
||||||
.expect(CLAP)
|
|
||||||
.parse()
|
|
||||||
.expect(VALIDATOR);
|
|
||||||
let bind_port = args.value_of(PORT).expect(CLAP).parse().expect(VALIDATOR);
|
let bind_port = args.value_of(PORT).expect(CLAP).parse().expect(VALIDATOR);
|
||||||
|
|
||||||
let trust_identity = args.is_present(TRUST_IDENTITY);
|
let trust_identity = args.is_present(TRUST_IDENTITY);
|
||||||
|
|
||||||
sausagewiki::main(db_file, bind_host, bind_port, trust_identity)
|
sausagewiki::main(
|
||||||
|
db_file,
|
||||||
|
bind_host,
|
||||||
|
bind_port,
|
||||||
|
trust_identity,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use diff;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Chunk<'a, Item: 'a + Debug + PartialEq + Copy>(
|
pub struct Chunk<'a, Item: 'a + Debug + PartialEq + Copy>(
|
||||||
pub &'a [diff::Result<Item>],
|
pub &'a [diff::Result<Item>],
|
||||||
pub &'a [diff::Result<Item>],
|
pub &'a [diff::Result<Item>]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use diff;
|
||||||
use diff::Result::*;
|
use diff::Result::*;
|
||||||
|
|
||||||
use super::chunk::Chunk;
|
use super::chunk::Chunk;
|
||||||
|
|
||||||
pub struct ChunkIterator<'a, Item>
|
pub struct ChunkIterator<'a, Item>
|
||||||
where
|
where
|
||||||
Item: 'a + Debug + PartialEq,
|
Item: 'a + Debug + PartialEq
|
||||||
{
|
{
|
||||||
left: &'a [diff::Result<Item>],
|
left: &'a [diff::Result<Item>],
|
||||||
right: &'a [diff::Result<Item>],
|
right: &'a [diff::Result<Item>],
|
||||||
|
@ -14,19 +15,16 @@ where
|
||||||
|
|
||||||
impl<'a, Item> ChunkIterator<'a, Item>
|
impl<'a, Item> ChunkIterator<'a, Item>
|
||||||
where
|
where
|
||||||
Item: 'a + Debug + PartialEq + Eq,
|
Item: 'a + Debug + PartialEq + Eq
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(left: &'a [diff::Result<Item>], right: &'a [diff::Result<Item>]) -> ChunkIterator<'a, Item> {
|
||||||
left: &'a [diff::Result<Item>],
|
|
||||||
right: &'a [diff::Result<Item>],
|
|
||||||
) -> ChunkIterator<'a, Item> {
|
|
||||||
ChunkIterator { left, right }
|
ChunkIterator { left, right }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Item> Iterator for ChunkIterator<'a, Item>
|
impl<'a, Item> Iterator for ChunkIterator<'a, Item>
|
||||||
where
|
where
|
||||||
Item: 'a + Debug + PartialEq + Copy,
|
Item: 'a + Debug + PartialEq + Copy
|
||||||
{
|
{
|
||||||
type Item = Chunk<'a, Item>;
|
type Item = Chunk<'a, Item>;
|
||||||
|
|
||||||
|
@ -48,18 +46,18 @@ where
|
||||||
match (self.left.get(li), self.right.get(ri)) {
|
match (self.left.get(li), self.right.get(ri)) {
|
||||||
(Some(&Right(_)), _) => {
|
(Some(&Right(_)), _) => {
|
||||||
li += 1;
|
li += 1;
|
||||||
}
|
},
|
||||||
(_, Some(&Right(_))) => {
|
(_, Some(&Right(_))) => {
|
||||||
ri += 1;
|
ri += 1;
|
||||||
}
|
},
|
||||||
(Some(&Left(_)), Some(_)) => {
|
(Some(&Left(_)), Some(_)) => {
|
||||||
li += 1;
|
li += 1;
|
||||||
ri += 1;
|
ri += 1;
|
||||||
}
|
},
|
||||||
(Some(_), Some(&Left(_))) => {
|
(Some(_), Some(&Left(_))) => {
|
||||||
li += 1;
|
li += 1;
|
||||||
ri += 1;
|
ri += 1;
|
||||||
}
|
},
|
||||||
(Some(&Both(..)), Some(&Both(..))) => {
|
(Some(&Both(..)), Some(&Both(..))) => {
|
||||||
let chunk = Chunk(&self.left[..li], &self.right[..ri]);
|
let chunk = Chunk(&self.left[..li], &self.right[..ri]);
|
||||||
self.left = &self.left[li..];
|
self.left = &self.left[li..];
|
||||||
|
@ -67,7 +65,7 @@ where
|
||||||
return Some(chunk);
|
return Some(chunk);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if !self.left.is_empty() || !self.right.is_empty() {
|
if self.left.len() > 0 || self.right.len() > 0 {
|
||||||
let chunk = Chunk(self.left, self.right);
|
let chunk = Chunk(self.left, self.right);
|
||||||
self.left = &self.left[self.left.len()..];
|
self.left = &self.left[self.left.len()..];
|
||||||
self.right = &self.right[self.right.len()..];
|
self.right = &self.right[self.right.len()..];
|
||||||
|
@ -83,6 +81,7 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diff;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_case() {
|
fn simple_case() {
|
||||||
|
@ -95,16 +94,13 @@ mod test {
|
||||||
|
|
||||||
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(vec![
|
||||||
vec![
|
Chunk(&oa[0.. 3], &ob[0.. 3]),
|
||||||
Chunk(&oa[0..3], &ob[0..3]),
|
Chunk(&oa[3.. 6], &ob[3.. 3]),
|
||||||
Chunk(&oa[3..6], &ob[3..3]),
|
Chunk(&oa[6.. 9], &ob[3.. 6]),
|
||||||
Chunk(&oa[6..9], &ob[3..6]),
|
Chunk(&oa[9.. 9], &ob[6.. 9]),
|
||||||
Chunk(&oa[9..9], &ob[6..9]),
|
Chunk(&oa[9..12], &ob[9..12]),
|
||||||
Chunk(&oa[9..12], &ob[9..12]),
|
], chunks);
|
||||||
],
|
|
||||||
chunks
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -117,14 +113,11 @@ mod test {
|
||||||
let ob = diff::chars(o, b);
|
let ob = diff::chars(o, b);
|
||||||
|
|
||||||
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(vec![
|
||||||
vec![
|
Chunk(&oa[0.. 3], &ob[0.. 3]),
|
||||||
Chunk(&oa[0..3], &ob[0..3]),
|
Chunk(&oa[3.. 9], &ob[3.. 9]),
|
||||||
Chunk(&oa[3..9], &ob[3..9]),
|
Chunk(&oa[9..12], &ob[9..12]),
|
||||||
Chunk(&oa[9..12], &ob[9..12]),
|
], chunks);
|
||||||
],
|
|
||||||
chunks
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -137,10 +130,10 @@ mod test {
|
||||||
let ob = diff::chars(o, b);
|
let ob = diff::chars(o, b);
|
||||||
|
|
||||||
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(vec![
|
||||||
vec![Chunk(&oa[0..9], &ob[0..9]), Chunk(&oa[9..9], &ob[9..12]),],
|
Chunk(&oa[0..9], &ob[0.. 9]),
|
||||||
chunks
|
Chunk(&oa[9..9], &ob[9..12]),
|
||||||
);
|
], chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -153,10 +146,10 @@ mod test {
|
||||||
let ob = diff::chars(o, b);
|
let ob = diff::chars(o, b);
|
||||||
|
|
||||||
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(vec![
|
||||||
vec![Chunk(&oa[0..6], &ob[0..6]), Chunk(&oa[6..9], &ob[6..12]),],
|
Chunk(&oa[0..6], &ob[0.. 6]),
|
||||||
chunks
|
Chunk(&oa[6..9], &ob[6..12]),
|
||||||
);
|
], chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -169,6 +162,8 @@ mod test {
|
||||||
let ob = diff::chars(o, b);
|
let ob = diff::chars(o, b);
|
||||||
|
|
||||||
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
|
||||||
assert_eq!(vec![Chunk(&oa[0..6], &ob[0..6]),], chunks);
|
assert_eq!(vec![
|
||||||
|
Chunk(&oa[0..6], &ob[0..6]),
|
||||||
|
], chunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
222
src/merge/mod.rs
222
src/merge/mod.rs
|
@ -1,12 +1,14 @@
|
||||||
mod chunk;
|
|
||||||
mod chunk_iterator;
|
mod chunk_iterator;
|
||||||
|
mod chunk;
|
||||||
mod output;
|
mod output;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use diff;
|
||||||
|
|
||||||
use self::chunk_iterator::ChunkIterator;
|
use self::chunk_iterator::ChunkIterator;
|
||||||
use self::output::Output::Resolved;
|
|
||||||
use self::output::*;
|
use self::output::*;
|
||||||
|
use self::output::Output::Resolved;
|
||||||
|
|
||||||
pub use self::output::Output;
|
pub use self::output::Output;
|
||||||
|
|
||||||
|
@ -17,12 +19,12 @@ pub enum MergeResult<Item: Debug + PartialEq> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MergeResult<&'a str> {
|
impl<'a> MergeResult<&'a str> {
|
||||||
pub fn into_strings(self) -> MergeResult<String> {
|
pub fn to_strings(self) -> MergeResult<String> {
|
||||||
match self {
|
match self {
|
||||||
MergeResult::Clean(x) => MergeResult::Clean(x),
|
MergeResult::Clean(x) => MergeResult::Clean(x),
|
||||||
MergeResult::Conflicted(x) => {
|
MergeResult::Conflicted(x) => MergeResult::Conflicted(
|
||||||
MergeResult::Conflicted(x.into_iter().map(Output::into_strings).collect())
|
x.into_iter().map(Output::to_strings).collect()
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,21 +33,23 @@ impl MergeResult<String> {
|
||||||
pub fn flatten(self) -> String {
|
pub fn flatten(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
MergeResult::Clean(x) => x,
|
MergeResult::Clean(x) => x,
|
||||||
MergeResult::Conflicted(x) => x
|
MergeResult::Conflicted(x) => {
|
||||||
.into_iter()
|
x.into_iter()
|
||||||
.flat_map(|out| match out {
|
.flat_map(|out| match out {
|
||||||
Output::Conflict(a, _o, b) => {
|
Output::Conflict(a, _o, b) => {
|
||||||
let mut x: Vec<String> = vec![];
|
let mut x: Vec<String> = vec![];
|
||||||
x.push("<<<<<<< Your changes:\n".into());
|
x.push("<<<<<<< Your changes:\n".into());
|
||||||
x.extend(a.into_iter().map(|x| format!("{}\n", x)));
|
x.extend(a.into_iter().map(|x| format!("{}\n", x)));
|
||||||
x.push("======= Their changes:\n".into());
|
x.push("======= Their changes:\n".into());
|
||||||
x.extend(b.into_iter().map(|x| format!("{}\n", x)));
|
x.extend(b.into_iter().map(|x| format!("{}\n", x)));
|
||||||
x.push(">>>>>>> Conflict ends here\n".into());
|
x.push(">>>>>>> Conflict ends here\n".into());
|
||||||
x
|
x
|
||||||
}
|
},
|
||||||
Output::Resolved(x) => x.into_iter().map(|x| format!("{}\n", x)).collect(),
|
Output::Resolved(x) =>
|
||||||
})
|
x.into_iter().map(|x| format!("{}\n", x)).collect(),
|
||||||
.collect(),
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,21 +58,22 @@ impl MergeResult<char> {
|
||||||
pub fn flatten(self) -> String {
|
pub fn flatten(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
MergeResult::Clean(x) => x,
|
MergeResult::Clean(x) => x,
|
||||||
MergeResult::Conflicted(x) => x
|
MergeResult::Conflicted(x) => {
|
||||||
.into_iter()
|
x.into_iter()
|
||||||
.flat_map(|out| match out {
|
.flat_map(|out| match out {
|
||||||
Output::Conflict(a, _o, b) => {
|
Output::Conflict(a, _o, b) => {
|
||||||
let mut x: Vec<char> = vec![];
|
let mut x: Vec<char> = vec![];
|
||||||
x.push('<');
|
x.push('<');
|
||||||
x.extend(a);
|
x.extend(a);
|
||||||
x.push('|');
|
x.push('|');
|
||||||
x.extend(b);
|
x.extend(b);
|
||||||
x.push('>');
|
x.push('>');
|
||||||
x
|
x
|
||||||
}
|
},
|
||||||
Output::Resolved(x) => x,
|
Output::Resolved(x) => x,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +85,7 @@ pub fn merge_lines<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<&'a st
|
||||||
let chunks = ChunkIterator::new(&oa, &ob);
|
let chunks = ChunkIterator::new(&oa, &ob);
|
||||||
let hunks: Vec<_> = chunks.map(resolve).collect();
|
let hunks: Vec<_> = chunks.map(resolve).collect();
|
||||||
|
|
||||||
let clean = hunks.iter().all(|x| matches!(x, Resolved(..)));
|
let clean = hunks.iter().all(|x| match x { &Resolved(..) => true, _ => false });
|
||||||
|
|
||||||
if clean {
|
if clean {
|
||||||
MergeResult::Clean(
|
MergeResult::Clean(
|
||||||
|
@ -88,10 +93,10 @@ pub fn merge_lines<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<&'a st
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|x| match x {
|
.flat_map(|x| match x {
|
||||||
Resolved(y) => y.into_iter(),
|
Resolved(y) => y.into_iter(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MergeResult::Conflicted(hunks)
|
MergeResult::Conflicted(hunks)
|
||||||
|
@ -105,7 +110,7 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
|
||||||
let chunks = ChunkIterator::new(&oa, &ob);
|
let chunks = ChunkIterator::new(&oa, &ob);
|
||||||
let hunks: Vec<_> = chunks.map(resolve).collect();
|
let hunks: Vec<_> = chunks.map(resolve).collect();
|
||||||
|
|
||||||
let clean = hunks.iter().all(|x| matches!(x, Resolved(..)));
|
let clean = hunks.iter().all(|x| match x { &Resolved(..) => true, _ => false });
|
||||||
|
|
||||||
if clean {
|
if clean {
|
||||||
MergeResult::Clean(
|
MergeResult::Clean(
|
||||||
|
@ -113,9 +118,9 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|x| match x {
|
.flat_map(|x| match x {
|
||||||
Resolved(y) => y.into_iter(),
|
Resolved(y) => y.into_iter(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!()
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MergeResult::Conflicted(hunks)
|
MergeResult::Conflicted(hunks)
|
||||||
|
@ -124,11 +129,11 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use diff;
|
||||||
|
|
||||||
use super::output::Output::*;
|
|
||||||
use super::output::*;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use super::output::*;
|
||||||
|
use super::output::Output::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_case() {
|
fn simple_case() {
|
||||||
|
@ -140,141 +145,106 @@ mod test {
|
||||||
chunks.map(resolve).collect()
|
chunks.map(resolve).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(vec![
|
||||||
vec![
|
Resolved("aaa".chars().collect()),
|
||||||
Resolved("aaa".chars().collect()),
|
Resolved("xxx".chars().collect()),
|
||||||
Resolved("xxx".chars().collect()),
|
Resolved("bbb".chars().collect()),
|
||||||
Resolved("bbb".chars().collect()),
|
Resolved("yyy".chars().collect()),
|
||||||
Resolved("yyy".chars().collect()),
|
Resolved("ccc".chars().collect()),
|
||||||
Resolved("ccc".chars().collect()),
|
], merge_chars(
|
||||||
],
|
"aaaxxxbbbccc",
|
||||||
merge_chars("aaaxxxbbbccc", "aaabbbccc", "aaabbbyyyccc",)
|
"aaabbbccc",
|
||||||
);
|
"aaabbbyyyccc",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clean_case() {
|
fn clean_case() {
|
||||||
assert_eq!(
|
assert_eq!(MergeResult::Clean(indoc!("
|
||||||
MergeResult::Clean(
|
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
bbb
|
bbb
|
||||||
yyy
|
yyy
|
||||||
ccc
|
ccc
|
||||||
"
|
").into()), merge_lines(
|
||||||
)
|
indoc!("
|
||||||
.into()
|
|
||||||
),
|
|
||||||
merge_lines(
|
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
bbb
|
bbb
|
||||||
yyy
|
yyy
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clean_case_chars() {
|
fn clean_case_chars() {
|
||||||
assert_eq!(
|
assert_eq!(MergeResult::Clean("Title".into()), merge_chars(
|
||||||
MergeResult::Clean("Title".into()),
|
"Titlle",
|
||||||
merge_chars("Titlle", "titlle", "title",)
|
"titlle",
|
||||||
);
|
"title",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn false_conflict() {
|
fn false_conflict() {
|
||||||
assert_eq!(
|
assert_eq!(MergeResult::Clean(indoc!("
|
||||||
MergeResult::Clean(
|
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
ccc
|
ccc
|
||||||
"
|
").into()), merge_lines(
|
||||||
)
|
indoc!("
|
||||||
.into()
|
|
||||||
),
|
|
||||||
merge_lines(
|
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn true_conflict() {
|
fn true_conflict() {
|
||||||
assert_eq!(
|
assert_eq!(MergeResult::Conflicted(vec![
|
||||||
MergeResult::Conflicted(vec![
|
Resolved(vec!["aaa"]),
|
||||||
Resolved(vec!["aaa"]),
|
Conflict(vec!["xxx"], vec![], vec!["yyy"]),
|
||||||
Conflict(vec!["xxx"], vec![], vec!["yyy"]),
|
Resolved(vec!["bbb", "ccc", ""]),
|
||||||
Resolved(vec!["bbb", "ccc", ""]),
|
]), merge_lines(
|
||||||
]),
|
indoc!("
|
||||||
merge_lines(
|
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
xxx
|
xxx
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
indoc!("
|
||||||
indoc!(
|
|
||||||
"
|
|
||||||
aaa
|
aaa
|
||||||
yyy
|
yyy
|
||||||
bbb
|
bbb
|
||||||
ccc
|
ccc
|
||||||
"
|
"),
|
||||||
),
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use diff;
|
||||||
use diff::Result::*;
|
use diff::Result::*;
|
||||||
|
|
||||||
use super::chunk::Chunk;
|
use super::chunk::Chunk;
|
||||||
|
@ -11,7 +12,7 @@ pub enum Output<Item: Debug + PartialEq> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Output<&'a str> {
|
impl<'a> Output<&'a str> {
|
||||||
pub fn into_strings(self) -> Output<String> {
|
pub fn to_strings(self) -> Output<String> {
|
||||||
match self {
|
match self {
|
||||||
Output::Resolved(x) => Output::Resolved(x.into_iter().map(str::to_string).collect()),
|
Output::Resolved(x) => Output::Resolved(x.into_iter().map(str::to_string).collect()),
|
||||||
Output::Conflict(a, o, b) => Output::Conflict(
|
Output::Conflict(a, o, b) => Output::Conflict(
|
||||||
|
@ -26,10 +27,10 @@ impl<'a> Output<&'a str> {
|
||||||
fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
|
fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
|
||||||
operations
|
operations
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|x| match *x {
|
.filter_map(|x| match x {
|
||||||
Both(y, _) => Some(y),
|
&Both(y, _) => Some(y),
|
||||||
Left(y) => Some(y),
|
&Left(y) => Some(y),
|
||||||
Right(_) => None,
|
&Right(_) => None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -37,16 +38,21 @@ fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
|
||||||
fn choose_right<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
|
fn choose_right<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
|
||||||
operations
|
operations
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|x| match *x {
|
.filter_map(|x| match x {
|
||||||
Both(_, y) => Some(y),
|
&Both(_, y) => Some(y),
|
||||||
Left(_) => None,
|
&Left(_) => None,
|
||||||
Right(y) => Some(y),
|
&Right(y) => Some(y),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_change<Item>(operations: &[diff::Result<Item>]) -> bool {
|
fn no_change<Item>(operations: &[diff::Result<Item>]) -> bool {
|
||||||
operations.iter().all(|x| matches!(x, Both(..)))
|
operations
|
||||||
|
.iter()
|
||||||
|
.all(|x| match x {
|
||||||
|
&Both(..) => true,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve<'a, Item: 'a + Debug + PartialEq + Copy>(chunk: Chunk<'a, Item>) -> Output<Item> {
|
pub fn resolve<'a, Item: 'a + Debug + PartialEq + Copy>(chunk: Chunk<'a, Item>) -> Output<Item> {
|
||||||
|
@ -63,51 +69,92 @@ pub fn resolve<'a, Item: 'a + Debug + PartialEq + Copy>(chunk: Chunk<'a, Item>)
|
||||||
return Output::Resolved(choose_right(chunk.0));
|
return Output::Resolved(choose_right(chunk.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Output::Conflict(
|
return Output::Conflict(
|
||||||
choose_right(chunk.0),
|
choose_right(chunk.0),
|
||||||
choose_left(chunk.0),
|
choose_left(chunk.0),
|
||||||
choose_right(chunk.1),
|
choose_right(chunk.1),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use diff::Result::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn empty() {
|
||||||
assert_eq!(Output::Resolved(vec![]), resolve::<i32>(Chunk(&[], &[])));
|
assert_eq!(
|
||||||
|
Output::Resolved(vec![]),
|
||||||
|
resolve::<i32>(Chunk(&[], &[]))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same() {
|
fn same() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Output::Resolved(vec![1]),
|
Output::Resolved(vec![
|
||||||
resolve::<i32>(Chunk(&[Both(1, 1)], &[Both(1, 1)]))
|
1
|
||||||
|
]),
|
||||||
|
resolve::<i32>(Chunk(
|
||||||
|
&[Both(1, 1)],
|
||||||
|
&[Both(1, 1)]
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_left() {
|
fn only_left() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Output::Resolved(vec![2]),
|
Output::Resolved(vec![
|
||||||
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[]))
|
2
|
||||||
|
]),
|
||||||
|
resolve::<i32>(Chunk(
|
||||||
|
&[
|
||||||
|
Left(1),
|
||||||
|
Right(2)
|
||||||
|
],
|
||||||
|
&[]
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn false_conflict() {
|
fn false_conflict() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Output::Resolved(vec![2]),
|
Output::Resolved(vec![
|
||||||
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[Left(1), Right(2)],))
|
2
|
||||||
|
]),
|
||||||
|
resolve::<i32>(Chunk(
|
||||||
|
&[
|
||||||
|
Left(1),
|
||||||
|
Right(2)
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
Left(1),
|
||||||
|
Right(2)
|
||||||
|
],
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn real_conflict() {
|
fn real_conflict() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Output::Conflict(vec![2], vec![1], vec![3],),
|
Output::Conflict(
|
||||||
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[Left(1), Right(3)],))
|
vec![2],
|
||||||
|
vec![1],
|
||||||
|
vec![3],
|
||||||
|
),
|
||||||
|
resolve::<i32>(Chunk(
|
||||||
|
&[
|
||||||
|
Left(1),
|
||||||
|
Right(2)
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
Left(1),
|
||||||
|
Right(3)
|
||||||
|
],
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::theme::Theme;
|
use chrono;
|
||||||
|
|
||||||
fn slug_link(slug: &str) -> &str {
|
fn slug_link(slug: &str) -> &str {
|
||||||
if slug.is_empty() {
|
if slug.is_empty() {
|
||||||
|
@ -23,14 +23,10 @@ pub struct ArticleRevision {
|
||||||
pub latest: bool,
|
pub latest: bool,
|
||||||
|
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
pub theme: Theme,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArticleRevision {
|
impl ArticleRevision {
|
||||||
pub fn link(&self) -> &str {
|
pub fn link(&self) -> &str { slug_link(&self.slug) }
|
||||||
slug_link(&self.slug)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Queryable)]
|
#[derive(Debug, PartialEq, Queryable)]
|
||||||
|
@ -47,14 +43,10 @@ pub struct ArticleRevisionStub {
|
||||||
pub latest: bool,
|
pub latest: bool,
|
||||||
|
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
pub theme: Theme,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArticleRevisionStub {
|
impl ArticleRevisionStub {
|
||||||
pub fn link(&self) -> &str {
|
pub fn link(&self) -> &str { slug_link(&self.slug) }
|
||||||
slug_link(&self.slug)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use diesel::sql_types::Text;
|
use diesel::sql_types::Text;
|
||||||
|
@ -71,7 +63,5 @@ pub struct SearchResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchResult {
|
impl SearchResult {
|
||||||
pub fn link(&self) -> &str {
|
pub fn link(&self) -> &str { slug_link(&self.slug) }
|
||||||
slug_link(&self.slug)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
use pulldown_cmark::Event::{End, Text};
|
use pulldown_cmark::{Parser, Tag, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML};
|
||||||
use pulldown_cmark::{html, Parser, Tag, OPTION_DISABLE_HTML, OPTION_ENABLE_TABLES};
|
use pulldown_cmark::Event::{Text, End};
|
||||||
use slug::slugify;
|
|
||||||
|
|
||||||
fn slugify_link(text: &str, title: &str) -> Option<(String, String)> {
|
|
||||||
Some((slugify(text), title.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parser(src: &str) -> Parser {
|
|
||||||
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
|
||||||
Parser::new_with_broken_link_callback(src, opts, Some(&slugify_link))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_markdown(src: &str) -> String {
|
pub fn render_markdown(src: &str) -> String {
|
||||||
let p = parser(src);
|
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
||||||
|
let p = Parser::new_ext(src, opts);
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
html::push_html(&mut buf, p);
|
html::push_html(&mut buf, p);
|
||||||
buf
|
buf
|
||||||
|
@ -23,43 +14,22 @@ fn is_html_special(c: char) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_markdown_for_fts(src: &str) -> String {
|
pub fn render_markdown_for_fts(src: &str) -> String {
|
||||||
let p = parser(src);
|
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
|
||||||
|
let p = Parser::new_ext(src, opts);
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
|
||||||
for event in p {
|
for event in p {
|
||||||
match event {
|
match event {
|
||||||
// As far as I understand this is a basic
|
Text(text) =>
|
||||||
// sanitizing to prevent HTML from
|
buf.push_str(&text.replace(is_html_special, " ")),
|
||||||
// appearing in page.
|
|
||||||
Text(text) => buf.push_str(&text.replace(is_html_special, " ")),
|
|
||||||
// Footnote links maybe?
|
|
||||||
End(Tag::Link(uri, _title)) => {
|
End(Tag::Link(uri, _title)) => {
|
||||||
buf.push_str(" (");
|
buf.push_str(" (");
|
||||||
buf.push_str(&uri.replace(is_html_special, " "));
|
buf.push_str(&uri.replace(is_html_special, " "));
|
||||||
buf.push_str(") ");
|
buf.push_str(") ");
|
||||||
}
|
}
|
||||||
_ => buf.push(' '),
|
_ => buf.push_str(" "),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn slug_link() {
|
|
||||||
let actual = render_markdown("[Slug link]");
|
|
||||||
let expected = "<p><a href=\"slug-link\" title=\"Slug link\">Slug link</a></p>\n";
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn footnote_links() {
|
|
||||||
let actual = render_markdown("[Link]\n\n[Link]: target");
|
|
||||||
let expected = "<p><a href=\"target\">Link</a></p>\n";
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use crate::build_config;
|
use build_config;
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
#[derive(Licenses)]
|
#[derive(Licenses)]
|
||||||
pub struct AboutResource;
|
pub struct AboutResource;
|
||||||
|
@ -28,20 +28,20 @@ impl License {
|
||||||
fn link(&self) -> &'static str {
|
fn link(&self) -> &'static str {
|
||||||
use self::License::*;
|
use self::License::*;
|
||||||
match self {
|
match self {
|
||||||
Bsd3Clause => "bsd-3-clause",
|
&Bsd3Clause => "bsd-3-clause",
|
||||||
Mit => "mit",
|
&Mit => "mit",
|
||||||
Mpl2 => "mpl2",
|
&Mpl2 => "mpl2",
|
||||||
Ofl11 => "sil-ofl-1.1",
|
&Ofl11 => "sil-ofl-1.1",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
use self::License::*;
|
use self::License::*;
|
||||||
match self {
|
match self {
|
||||||
Bsd3Clause => "BSD-3-Clause",
|
&Bsd3Clause => "BSD-3-Clause",
|
||||||
Mit => "MIT",
|
&Mit => "MIT",
|
||||||
Mpl2 => "MPL2",
|
&Mpl2 => "MPL2",
|
||||||
Ofl11 => "OFL-1.1",
|
&Ofl11 => "OFL-1.1",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,15 +54,13 @@ struct LicenseInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/about.html"]
|
#[template="templates/about.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
deps: &'a [LicenseInfo],
|
deps: &'a [LicenseInfo]
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Template<'a> {
|
impl<'a> Template<'a> {
|
||||||
fn version(&self) -> &str {
|
fn version(&self) -> &str { &build_config::VERSION }
|
||||||
&build_config::VERSION
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource for AboutResource {
|
impl Resource for AboutResource {
|
||||||
|
@ -72,27 +70,25 @@ impl Resource for AboutResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(head.and_then(move |head| {
|
Box::new(head
|
||||||
Ok(head.with_body(
|
.and_then(move |head| {
|
||||||
system_page(
|
Ok(head
|
||||||
None, // Hmm, should perhaps accept `base` as argument
|
.with_body(Layout {
|
||||||
"About Sausagewiki",
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
Template {
|
title: "About Sausagewiki",
|
||||||
deps: *LICENSE_INFOS,
|
body: &Template {
|
||||||
},
|
deps: &*LICENSE_INFOS
|
||||||
)
|
},
|
||||||
.to_string(),
|
}.to_string()))
|
||||||
))
|
}))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
use chrono::{DateTime, Local, TimeZone};
|
use chrono::{TimeZone, DateTime, Local};
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::{ContentType, Location};
|
use hyper::header::{ContentType, Location};
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_json;
|
||||||
|
use serde_urlencoded;
|
||||||
|
|
||||||
use crate::assets::ScriptJs;
|
use assets::ScriptJs;
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
use crate::site::Layout;
|
use site::Layout;
|
||||||
use crate::state::{RebaseConflict, State, UpdateResult};
|
use state::{State, UpdateResult, RebaseConflict};
|
||||||
use crate::theme::{self, Theme};
|
use web::{Resource, ResponseFuture};
|
||||||
use crate::web::{Resource, ResponseFuture};
|
|
||||||
|
|
||||||
use super::changes_resource::QueryParameters;
|
use super::changes_resource::QueryParameters;
|
||||||
|
|
||||||
struct SelectableTheme {
|
|
||||||
theme: Theme,
|
|
||||||
selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/article.html"]
|
#[template="templates/article.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
revision: i32,
|
revision: i32,
|
||||||
last_updated: Option<&'a str>,
|
last_updated: Option<&'a str>,
|
||||||
|
@ -30,12 +26,11 @@ struct Template<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
raw: &'a str,
|
raw: &'a str,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
themes: &'a [SelectableTheme],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Template<'a> {
|
impl<'a> Template<'a> {
|
||||||
fn script_js(&self) -> &'static str {
|
fn script_js_checksum(&self) -> &'static str {
|
||||||
ScriptJs::resource_name()
|
ScriptJs::checksum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +39,6 @@ struct UpdateArticle {
|
||||||
base_revision: i32,
|
base_revision: i32,
|
||||||
title: String,
|
title: String,
|
||||||
body: String,
|
body: String,
|
||||||
theme: Option<Theme>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ArticleResource {
|
pub struct ArticleResource {
|
||||||
|
@ -56,12 +50,7 @@ pub struct ArticleResource {
|
||||||
|
|
||||||
impl ArticleResource {
|
impl ArticleResource {
|
||||||
pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self {
|
pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self {
|
||||||
Self {
|
Self { state, article_id, revision, edit }
|
||||||
state,
|
|
||||||
article_id,
|
|
||||||
revision,
|
|
||||||
edit,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,23 +70,12 @@ pub fn last_updated(article_id: i32, created: &DateTime<Local>, author: Option<&
|
||||||
|
|
||||||
Template {
|
Template {
|
||||||
created: &created.to_rfc2822(),
|
created: &created.to_rfc2822(),
|
||||||
article_history: &format!(
|
article_history: &format!("_changes{}", QueryParameters::default().article_id(Some(article_id)).into_link()),
|
||||||
"_changes{}",
|
|
||||||
QueryParameters::default()
|
|
||||||
.article_id(Some(article_id))
|
|
||||||
.into_link()
|
|
||||||
),
|
|
||||||
author: author.map(|author| Author {
|
author: author.map(|author| Author {
|
||||||
author,
|
author: &author,
|
||||||
history: format!(
|
history: format!("_changes{}", QueryParameters::default().author(Some(author.to_owned())).into_link()),
|
||||||
"_changes{}",
|
|
||||||
QueryParameters::default()
|
|
||||||
.author(Some(author.to_owned()))
|
|
||||||
.into_link()
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
}
|
}.to_string()
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource for ArticleResource {
|
impl Resource for ArticleResource {
|
||||||
|
@ -107,50 +85,38 @@ impl Resource for ArticleResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
let data = self
|
let data = self.state.get_article_revision(self.article_id, self.revision)
|
||||||
.state
|
|
||||||
.get_article_revision(self.article_id, self.revision)
|
|
||||||
.map(|x| x.expect("Data model guarantees that this exists"));
|
.map(|x| x.expect("Data model guarantees that this exists"));
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(data.join(head).and_then(move |(data, head)| {
|
Box::new(data.join(head)
|
||||||
Ok(head.with_body(
|
.and_then(move |(data, head)| {
|
||||||
Layout {
|
Ok(head
|
||||||
base: None, // Hmm, should perhaps accept `base` as argument
|
.with_body(Layout {
|
||||||
title: &data.title,
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
theme: data.theme,
|
|
||||||
body: &Template {
|
|
||||||
revision: data.revision,
|
|
||||||
last_updated: Some(&last_updated(
|
|
||||||
data.article_id,
|
|
||||||
&Local.from_utc_datetime(&data.created),
|
|
||||||
data.author.as_deref(),
|
|
||||||
)),
|
|
||||||
edit: self.edit,
|
|
||||||
cancel_url: Some(data.link()),
|
|
||||||
title: &data.title,
|
title: &data.title,
|
||||||
raw: &data.body,
|
body: &Template {
|
||||||
rendered: render_markdown(&data.body),
|
revision: data.revision,
|
||||||
themes: &theme::THEMES
|
last_updated: Some(&last_updated(
|
||||||
.iter()
|
data.article_id,
|
||||||
.map(|&x| SelectableTheme {
|
&Local.from_utc_datetime(&data.created),
|
||||||
theme: x,
|
data.author.as_ref().map(|x| &**x)
|
||||||
selected: x == data.theme,
|
)),
|
||||||
})
|
edit: self.edit,
|
||||||
.collect::<Vec<_>>(),
|
cancel_url: Some(data.link()),
|
||||||
},
|
title: &data.title,
|
||||||
}
|
raw: &data.body,
|
||||||
.to_string(),
|
rendered: render_markdown(&data.body),
|
||||||
))
|
},
|
||||||
}))
|
}.to_string()))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
|
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
|
||||||
|
@ -159,7 +125,7 @@ impl Resource for ArticleResource {
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/article_contents.html"]
|
#[template="templates/article_contents.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
|
@ -172,84 +138,69 @@ impl Resource for ArticleResource {
|
||||||
revision: i32,
|
revision: i32,
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
body: &'a str,
|
body: &'a str,
|
||||||
theme: Theme,
|
|
||||||
rendered: &'a str,
|
rendered: &'a str,
|
||||||
last_updated: &'a str,
|
last_updated: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.concat2()
|
.concat2()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
|
.and_then(|body| {
|
||||||
.and_then(move |update: UpdateArticle| {
|
serde_urlencoded::from_bytes(&body)
|
||||||
self.state.update_article(
|
.map_err(Into::into)
|
||||||
self.article_id,
|
})
|
||||||
update.base_revision,
|
.and_then(move |update: UpdateArticle| {
|
||||||
update.title,
|
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
|
||||||
update.body,
|
})
|
||||||
identity,
|
.and_then(|updated| match updated {
|
||||||
update.theme,
|
UpdateResult::Success(updated) =>
|
||||||
)
|
Ok(Response::new()
|
||||||
})
|
|
||||||
.and_then(|updated| match updated {
|
|
||||||
UpdateResult::Success(updated) => Ok(Response::new()
|
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||||
.with_body(
|
.with_body(serde_json::to_string(&PutResponse {
|
||||||
serde_json::to_string(&PutResponse {
|
conflict: false,
|
||||||
conflict: false,
|
slug: &updated.slug,
|
||||||
slug: &updated.slug,
|
revision: updated.revision,
|
||||||
revision: updated.revision,
|
title: &updated.title,
|
||||||
|
body: &updated.body,
|
||||||
|
rendered: &Template {
|
||||||
title: &updated.title,
|
title: &updated.title,
|
||||||
body: &updated.body,
|
rendered: render_markdown(&updated.body),
|
||||||
theme: updated.theme,
|
}.to_string(),
|
||||||
rendered: &Template {
|
last_updated: &last_updated(
|
||||||
title: &updated.title,
|
updated.article_id,
|
||||||
rendered: render_markdown(&updated.body),
|
&Local.from_utc_datetime(&updated.created),
|
||||||
}
|
updated.author.as_ref().map(|x| &**x)
|
||||||
.to_string(),
|
),
|
||||||
last_updated: &last_updated(
|
}).expect("Should never fail"))
|
||||||
updated.article_id,
|
),
|
||||||
&Local.from_utc_datetime(&updated.created),
|
UpdateResult::RebaseConflict(RebaseConflict {
|
||||||
updated.author.as_deref(),
|
base_article, title, body
|
||||||
),
|
}) => {
|
||||||
})
|
let title = title.flatten();
|
||||||
.expect("Should never fail"),
|
let body = body.flatten();
|
||||||
)),
|
Ok(Response::new()
|
||||||
UpdateResult::RebaseConflict(RebaseConflict {
|
.with_status(hyper::StatusCode::Ok)
|
||||||
base_article,
|
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||||
title,
|
.with_body(serde_json::to_string(&PutResponse {
|
||||||
body,
|
conflict: true,
|
||||||
theme,
|
slug: &base_article.slug,
|
||||||
}) => {
|
revision: base_article.revision,
|
||||||
let title = title.flatten();
|
title: &title,
|
||||||
let body = body.flatten();
|
body: &body,
|
||||||
Ok(Response::new()
|
rendered: &Template {
|
||||||
.with_status(hyper::StatusCode::Ok)
|
title: &title,
|
||||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
rendered: render_markdown(&body),
|
||||||
.with_body(
|
}.to_string(),
|
||||||
serde_json::to_string(&PutResponse {
|
last_updated: &last_updated(
|
||||||
conflict: true,
|
base_article.article_id,
|
||||||
slug: &base_article.slug,
|
&Local.from_utc_datetime(&base_article.created),
|
||||||
revision: base_article.revision,
|
base_article.author.as_ref().map(|x| &**x)
|
||||||
title: &title,
|
),
|
||||||
body: &body,
|
}).expect("Should never fail"))
|
||||||
theme,
|
)
|
||||||
rendered: &Template {
|
}
|
||||||
title: &title,
|
})
|
||||||
rendered: render_markdown(&body),
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
last_updated: &last_updated(
|
|
||||||
base_article.article_id,
|
|
||||||
&Local.from_utc_datetime(&base_article.created),
|
|
||||||
base_article.author.as_deref(),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("Should never fail"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,67 +209,53 @@ impl Resource for ArticleResource {
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.concat2()
|
.concat2()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
|
.and_then(|body| {
|
||||||
.and_then(move |update: UpdateArticle| {
|
serde_urlencoded::from_bytes(&body)
|
||||||
self.state.update_article(
|
.map_err(Into::into)
|
||||||
self.article_id,
|
})
|
||||||
update.base_revision,
|
.and_then(move |update: UpdateArticle| {
|
||||||
update.title,
|
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
|
||||||
update.body,
|
})
|
||||||
identity,
|
.and_then(|updated| {
|
||||||
update.theme,
|
match updated {
|
||||||
)
|
|
||||||
})
|
|
||||||
.and_then(|updated| match updated {
|
|
||||||
UpdateResult::Success(updated) => Ok(Response::new()
|
UpdateResult::Success(updated) => Ok(Response::new()
|
||||||
.with_status(hyper::StatusCode::SeeOther)
|
.with_status(hyper::StatusCode::SeeOther)
|
||||||
.with_header(ContentType(TEXT_PLAIN.clone()))
|
.with_header(ContentType(TEXT_PLAIN.clone()))
|
||||||
.with_header(Location::new(updated.link().to_owned()))
|
.with_header(Location::new(updated.link().to_owned()))
|
||||||
.with_body("See other")),
|
.with_body("See other")
|
||||||
|
),
|
||||||
UpdateResult::RebaseConflict(RebaseConflict {
|
UpdateResult::RebaseConflict(RebaseConflict {
|
||||||
base_article,
|
base_article, title, body
|
||||||
title,
|
|
||||||
body,
|
|
||||||
theme,
|
|
||||||
}) => {
|
}) => {
|
||||||
let title = title.flatten();
|
let title = title.flatten();
|
||||||
let body = body.flatten();
|
let body = body.flatten();
|
||||||
Ok(Response::new()
|
Ok(Response::new()
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_header(ContentType(TEXT_HTML.clone()))
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_body(
|
.with_body(Layout {
|
||||||
Layout {
|
base: None,
|
||||||
base: None,
|
title: &title,
|
||||||
|
body: &Template {
|
||||||
|
revision: base_article.revision,
|
||||||
|
last_updated: Some(&last_updated(
|
||||||
|
base_article.article_id,
|
||||||
|
&Local.from_utc_datetime(&base_article.created),
|
||||||
|
base_article.author.as_ref().map(|x| &**x)
|
||||||
|
)),
|
||||||
|
edit: true,
|
||||||
|
cancel_url: Some(base_article.link()),
|
||||||
title: &title,
|
title: &title,
|
||||||
theme,
|
raw: &body,
|
||||||
body: &Template {
|
rendered: render_markdown(&body),
|
||||||
revision: base_article.revision,
|
},
|
||||||
last_updated: Some(&last_updated(
|
}.to_string())
|
||||||
base_article.article_id,
|
)
|
||||||
&Local.from_utc_datetime(&base_article.created),
|
|
||||||
base_article.author.as_deref(),
|
|
||||||
)),
|
|
||||||
edit: true,
|
|
||||||
cancel_url: Some(base_article.link()),
|
|
||||||
title: &title,
|
|
||||||
raw: &body,
|
|
||||||
rendered: render_markdown(&body),
|
|
||||||
themes: &theme::THEMES
|
|
||||||
.iter()
|
|
||||||
.map(|&x| SelectableTheme {
|
|
||||||
theme: x,
|
|
||||||
selected: x == theme,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
use chrono::{DateTime, Local, TimeZone};
|
use chrono::{TimeZone, DateTime, Local};
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::models;
|
use models;
|
||||||
use crate::rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
use super::changes_resource::QueryParameters;
|
use super::changes_resource::QueryParameters;
|
||||||
use super::diff_resource;
|
use super::diff_resource;
|
||||||
|
@ -24,12 +24,7 @@ impl ArticleRevisionResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timestamp_and_author(
|
pub fn timestamp_and_author(sequence_number: i32, article_id: i32, created: &DateTime<Local>, author: Option<&str>) -> String {
|
||||||
sequence_number: i32,
|
|
||||||
article_id: i32,
|
|
||||||
created: &DateTime<Local>,
|
|
||||||
author: Option<&str>,
|
|
||||||
) -> String {
|
|
||||||
struct Author<'a> {
|
struct Author<'a> {
|
||||||
author: &'a str,
|
author: &'a str,
|
||||||
history: String,
|
history: String,
|
||||||
|
@ -47,17 +42,15 @@ pub fn timestamp_and_author(
|
||||||
|
|
||||||
Template {
|
Template {
|
||||||
created: &created.to_rfc2822(),
|
created: &created.to_rfc2822(),
|
||||||
article_history: &format!(
|
article_history: &format!("_changes{}",
|
||||||
"_changes{}",
|
|
||||||
QueryParameters::default()
|
QueryParameters::default()
|
||||||
.pagination(pagination)
|
.pagination(pagination)
|
||||||
.article_id(Some(article_id))
|
.article_id(Some(article_id))
|
||||||
.into_link()
|
.into_link()
|
||||||
),
|
),
|
||||||
author: author.map(|author| Author {
|
author: author.map(|author| Author {
|
||||||
author,
|
author: &author,
|
||||||
history: format!(
|
history: format!("_changes{}",
|
||||||
"_changes{}",
|
|
||||||
QueryParameters::default()
|
QueryParameters::default()
|
||||||
.pagination(pagination)
|
.pagination(pagination)
|
||||||
.article_id(Some(article_id))
|
.article_id(Some(article_id))
|
||||||
|
@ -65,8 +58,7 @@ pub fn timestamp_and_author(
|
||||||
.into_link()
|
.into_link()
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
}
|
}.to_string()
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource for ArticleRevisionResource {
|
impl Resource for ArticleRevisionResource {
|
||||||
|
@ -76,56 +68,57 @@ impl Resource for ArticleRevisionResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/article_revision.html"]
|
#[template="templates/article_revision.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
link_current: &'a str,
|
link_current: &'a str,
|
||||||
timestamp_and_author: &'a str,
|
timestamp_and_author: &'a str,
|
||||||
diff_link: Option<String>,
|
diff_link: Option<String>,
|
||||||
|
|
||||||
|
title: &'a str,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
let data = self.data;
|
let data = self.data;
|
||||||
|
|
||||||
Box::new(head.and_then(move |head| {
|
Box::new(head
|
||||||
Ok(head.with_body(
|
.and_then(move |head|
|
||||||
system_page(
|
Ok(head
|
||||||
Some("../../"), // Hmm, should perhaps accept `base` as argument
|
.with_body(Layout {
|
||||||
&data.title,
|
base: Some("../../"), // Hmm, should perhaps accept `base` as argument
|
||||||
&Template {
|
title: &data.title,
|
||||||
link_current: &format!("_by_id/{}", data.article_id),
|
body: &Template {
|
||||||
timestamp_and_author: ×tamp_and_author(
|
link_current: &format!("_by_id/{}", data.article_id),
|
||||||
data.sequence_number,
|
timestamp_and_author: ×tamp_and_author(
|
||||||
data.article_id,
|
data.sequence_number,
|
||||||
&Local.from_utc_datetime(&data.created),
|
|
||||||
data.author.as_deref(),
|
|
||||||
),
|
|
||||||
diff_link: if data.revision > 1 {
|
|
||||||
Some(format!(
|
|
||||||
"_diff/{}?{}",
|
|
||||||
data.article_id,
|
data.article_id,
|
||||||
diff_resource::QueryParameters::new(
|
&Local.from_utc_datetime(&data.created),
|
||||||
data.revision as u32 - 1,
|
data.author.as_ref().map(|x| &**x)
|
||||||
data.revision as u32,
|
),
|
||||||
)
|
diff_link:
|
||||||
))
|
if data.revision > 1 {
|
||||||
} else {
|
Some(format!("_diff/{}?{}",
|
||||||
None
|
data.article_id,
|
||||||
|
diff_resource::QueryParameters::new(
|
||||||
|
data.revision as u32 - 1,
|
||||||
|
data.revision as u32,
|
||||||
|
)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
title: &data.title,
|
||||||
|
rendered: render_markdown(&data.body),
|
||||||
},
|
},
|
||||||
rendered: render_markdown(&data.body),
|
}.to_string()))
|
||||||
},
|
|
||||||
)
|
|
||||||
.to_string(),
|
|
||||||
))
|
))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
use futures::future::{done, finished};
|
use diesel;
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use futures::future::{done, finished};
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_urlencoded;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::schema::article_revisions;
|
use schema::article_revisions;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::state::State;
|
use state::State;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
use super::diff_resource;
|
use super::diff_resource;
|
||||||
use super::pagination::Pagination;
|
use super::pagination::Pagination;
|
||||||
|
@ -16,7 +18,7 @@ use super::TemporaryRedirectResource;
|
||||||
|
|
||||||
const DEFAULT_LIMIT: i32 = 30;
|
const DEFAULT_LIMIT: i32 = 30;
|
||||||
|
|
||||||
type BoxResource = Box<dyn Resource + Sync + Send>;
|
type BoxResource = Box<Resource + Sync + Send>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ChangesLookup {
|
pub struct ChangesLookup {
|
||||||
|
@ -38,16 +40,8 @@ pub struct QueryParameters {
|
||||||
impl QueryParameters {
|
impl QueryParameters {
|
||||||
pub fn pagination(self, pagination: Pagination<i32>) -> Self {
|
pub fn pagination(self, pagination: Pagination<i32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
after: if let Pagination::After(x) = pagination {
|
after: if let Pagination::After(x) = pagination { Some(x) } else { None },
|
||||||
Some(x)
|
before: if let Pagination::Before(x) = pagination { Some(x) } else { None },
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
before: if let Pagination::Before(x) = pagination {
|
|
||||||
Some(x)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,18 +56,14 @@ impl QueryParameters {
|
||||||
|
|
||||||
pub fn limit(self, limit: i32) -> Self {
|
pub fn limit(self, limit: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
limit: if limit != DEFAULT_LIMIT {
|
limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None },
|
||||||
Some(limit)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_link(self) -> String {
|
pub fn into_link(self) -> String {
|
||||||
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
|
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
|
||||||
if !args.is_empty() {
|
if args.len() > 0 {
|
||||||
format!("?{}", args)
|
format!("?{}", args)
|
||||||
} else {
|
} else {
|
||||||
"_changes".to_owned()
|
"_changes".to_owned()
|
||||||
|
@ -81,12 +71,14 @@ impl QueryParameters {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_query_config(
|
fn apply_query_config<'a>(
|
||||||
mut query: article_revisions::BoxedQuery<diesel::sqlite::Sqlite>,
|
mut query: article_revisions::BoxedQuery<'a, diesel::sqlite::Sqlite>,
|
||||||
article_id: Option<i32>,
|
article_id: Option<i32>,
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
) -> article_revisions::BoxedQuery<diesel::sqlite::Sqlite> {
|
)
|
||||||
|
-> article_revisions::BoxedQuery<'a, diesel::sqlite::Sqlite>
|
||||||
|
{
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
if let Some(article_id) = article_id {
|
if let Some(article_id) = article_id {
|
||||||
|
@ -102,16 +94,10 @@ fn apply_query_config(
|
||||||
|
|
||||||
impl ChangesLookup {
|
impl ChangesLookup {
|
||||||
pub fn new(state: State, show_authors: bool) -> ChangesLookup {
|
pub fn new(state: State, show_authors: bool) -> ChangesLookup {
|
||||||
Self {
|
Self { state, show_authors }
|
||||||
state,
|
|
||||||
show_authors,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(
|
pub fn lookup(&self, query: Option<&str>) -> Box<Future<Item=Option<BoxResource>, Error=::web::Error>> {
|
||||||
&self,
|
|
||||||
query: Option<&str>,
|
|
||||||
) -> Box<dyn Future<Item = Option<BoxResource>, Error = crate::web::Error>> {
|
|
||||||
use super::pagination;
|
use super::pagination;
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
@ -125,80 +111,54 @@ impl ChangesLookup {
|
||||||
|
|
||||||
let limit = match params.limit {
|
let limit = match params.limit {
|
||||||
None => Ok(DEFAULT_LIMIT),
|
None => Ok(DEFAULT_LIMIT),
|
||||||
Some(x) if (1..=100).contains(&x) => Ok(x),
|
Some(x) if 1 <= x && x <= 100 => Ok(x),
|
||||||
_ => Err("`limit` argument must be in range [1, 100]"),
|
_ => Err("`limit` argument must be in range [1, 100]"),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok((pagination, params.article_id, params.author, limit))
|
Ok((pagination, params.article_id, params.author, limit))
|
||||||
})())
|
})())
|
||||||
.and_then(move |(pagination, article_id, author, limit)| {
|
.and_then(move |(pagination, article_id, author, limit)| match pagination {
|
||||||
match pagination {
|
Pagination::After(x) => {
|
||||||
Pagination::After(x) => {
|
let author2 = author.clone();
|
||||||
let author2 = author.clone();
|
|
||||||
|
|
||||||
Box::new(
|
Box::new(state.query_article_revision_stubs(move |query| {
|
||||||
state
|
use diesel::prelude::*;
|
||||||
.query_article_revision_stubs(move |query| {
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
apply_query_config(query, article_id, author2, limit)
|
apply_query_config(query, article_id, author2, limit)
|
||||||
.filter(article_revisions::sequence_number.gt(x))
|
.filter(article_revisions::sequence_number.gt(x))
|
||||||
.order(article_revisions::sequence_number.asc())
|
.order(article_revisions::sequence_number.asc())
|
||||||
})
|
}).and_then(move |mut data| {
|
||||||
.and_then(move |mut data| {
|
let extra_element = if data.len() > limit as usize {
|
||||||
let extra_element = if data.len() > limit as usize {
|
data.pop()
|
||||||
data.pop()
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let args = QueryParameters {
|
let args =
|
||||||
after: None,
|
QueryParameters {
|
||||||
before: None,
|
after: None,
|
||||||
article_id,
|
before: None,
|
||||||
author,
|
article_id,
|
||||||
limit: None,
|
author,
|
||||||
}
|
limit: None,
|
||||||
.limit(limit);
|
}
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
Ok(Some(match extra_element {
|
Ok(Some(match extra_element {
|
||||||
Some(x) => Box::new(TemporaryRedirectResource::new(
|
Some(x) => Box::new(TemporaryRedirectResource::new(
|
||||||
args.pagination(Pagination::Before(x.sequence_number))
|
args
|
||||||
.into_link(),
|
.pagination(Pagination::Before(x.sequence_number))
|
||||||
))
|
.into_link()
|
||||||
as BoxResource,
|
)) as BoxResource,
|
||||||
None => Box::new(TemporaryRedirectResource::new(
|
None => Box::new(TemporaryRedirectResource::new(
|
||||||
args.into_link(),
|
args.into_link()
|
||||||
))
|
)) as BoxResource,
|
||||||
as BoxResource,
|
}))
|
||||||
}))
|
})) as Box<Future<Item=Option<BoxResource>, Error=::web::Error>>
|
||||||
}),
|
},
|
||||||
)
|
Pagination::Before(x) => Box::new(finished(Some(Box::new(ChangesResource::new(state, show_authors, Some(x), article_id, author, limit)) as BoxResource))),
|
||||||
as Box<
|
Pagination::None => Box::new(finished(Some(Box::new(ChangesResource::new(state, show_authors, None, article_id, author, limit)) as BoxResource))),
|
||||||
dyn Future<Item = Option<BoxResource>, Error = crate::web::Error>,
|
})
|
||||||
>
|
|
||||||
}
|
|
||||||
Pagination::Before(x) => {
|
|
||||||
Box::new(finished(Some(Box::new(ChangesResource::new(
|
|
||||||
state,
|
|
||||||
show_authors,
|
|
||||||
Some(x),
|
|
||||||
article_id,
|
|
||||||
author,
|
|
||||||
limit,
|
|
||||||
)) as BoxResource)))
|
|
||||||
}
|
|
||||||
Pagination::None => Box::new(finished(Some(Box::new(ChangesResource::new(
|
|
||||||
state,
|
|
||||||
show_authors,
|
|
||||||
None,
|
|
||||||
article_id,
|
|
||||||
author,
|
|
||||||
limit,
|
|
||||||
))
|
|
||||||
as BoxResource))),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,22 +173,8 @@ pub struct ChangesResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChangesResource {
|
impl ChangesResource {
|
||||||
pub fn new(
|
pub fn new(state: State, show_authors: bool, before: Option<i32>, article_id: Option<i32>, author: Option<String>, limit: i32) -> Self {
|
||||||
state: State,
|
Self { state, show_authors, before, article_id, author, limit }
|
||||||
show_authors: bool,
|
|
||||||
before: Option<i32>,
|
|
||||||
article_id: Option<i32>,
|
|
||||||
author: Option<String>,
|
|
||||||
limit: i32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
show_authors,
|
|
||||||
before,
|
|
||||||
article_id,
|
|
||||||
author,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_args(&self) -> QueryParameters {
|
fn query_args(&self) -> QueryParameters {
|
||||||
|
@ -250,15 +196,14 @@ impl Resource for ChangesResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{TimeZone, Local};
|
||||||
|
|
||||||
struct Row<'a> {
|
struct Row<'a> {
|
||||||
resource: &'a ChangesResource,
|
resource: &'a ChangesResource,
|
||||||
|
@ -279,8 +224,7 @@ impl Resource for ChangesResource {
|
||||||
|
|
||||||
impl<'a> Row<'a> {
|
impl<'a> Row<'a> {
|
||||||
fn author_link(&self) -> String {
|
fn author_link(&self) -> String {
|
||||||
self.resource
|
self.resource.query_args()
|
||||||
.query_args()
|
|
||||||
.pagination(Pagination::After(self.sequence_number))
|
.pagination(Pagination::After(self.sequence_number))
|
||||||
.author(self.author.clone())
|
.author(self.author.clone())
|
||||||
.into_link()
|
.into_link()
|
||||||
|
@ -293,7 +237,7 @@ impl Resource for ChangesResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/changes.html"]
|
#[template="templates/changes.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
resource: &'a ChangesResource,
|
resource: &'a ChangesResource,
|
||||||
|
|
||||||
|
@ -307,7 +251,7 @@ impl Resource for ChangesResource {
|
||||||
fn subject_clause(&self) -> String {
|
fn subject_clause(&self) -> String {
|
||||||
match self.resource.article_id {
|
match self.resource.article_id {
|
||||||
Some(x) => format!(" <a href=\"_by_id/{}\">this article</a>", x),
|
Some(x) => format!(" <a href=\"_by_id/{}\">this article</a>", x),
|
||||||
None => " the wiki".to_string(),
|
None => format!(" the wiki"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,25 +260,24 @@ impl Resource for ChangesResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_articles_link(&self) -> Option<String> {
|
fn all_articles_link(&self) -> Option<String> {
|
||||||
self.resource
|
self.resource.article_id.map(|_| {
|
||||||
.article_id
|
self.resource.query_args()
|
||||||
.map(|_| self.resource.query_args().article_id(None).into_link())
|
.article_id(None)
|
||||||
|
.into_link()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_authors_link(&self) -> Option<String> {
|
fn all_authors_link(&self) -> Option<String> {
|
||||||
self.resource
|
self.resource.author.as_ref().map(|_| {
|
||||||
.author
|
self.resource.query_args()
|
||||||
.as_ref()
|
.author(None)
|
||||||
.map(|_| self.resource.query_args().author(None).into_link())
|
.into_link()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (before, article_id, author, limit) = (
|
let (before, article_id, author, limit) =
|
||||||
self.before,
|
(self.before.clone(), self.article_id.clone(), self.author.clone(), self.limit);
|
||||||
self.article_id,
|
|
||||||
self.author.clone(),
|
|
||||||
self.limit,
|
|
||||||
);
|
|
||||||
let data = self.state.query_article_revision_stubs(move |query| {
|
let data = self.state.query_article_revision_stubs(move |query| {
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
@ -349,89 +292,78 @@ impl Resource for ChangesResource {
|
||||||
|
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(data.join(head).and_then(move |(mut data, head)| {
|
Box::new(data.join(head)
|
||||||
let extra_element = if data.len() > self.limit as usize {
|
.and_then(move |(mut data, head)| {
|
||||||
data.pop()
|
use std::iter::Iterator;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (newer, older) = match self.before {
|
let extra_element = if data.len() > self.limit as usize {
|
||||||
Some(x) => (
|
data.pop()
|
||||||
Some(NavLinks {
|
} else {
|
||||||
more: self
|
None
|
||||||
.query_args()
|
};
|
||||||
.pagination(Pagination::After(x - 1))
|
|
||||||
.into_link(),
|
|
||||||
end: self.query_args().pagination(Pagination::None).into_link(),
|
|
||||||
}),
|
|
||||||
extra_element.map(|_| NavLinks {
|
|
||||||
more: self
|
|
||||||
.query_args()
|
|
||||||
.pagination(Pagination::Before(data.last().unwrap().sequence_number))
|
|
||||||
.into_link(),
|
|
||||||
end: self
|
|
||||||
.query_args()
|
|
||||||
.pagination(Pagination::After(0))
|
|
||||||
.into_link(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
None => (
|
|
||||||
None,
|
|
||||||
extra_element.map(|_| NavLinks {
|
|
||||||
more: self
|
|
||||||
.query_args()
|
|
||||||
.pagination(Pagination::Before(data.last().unwrap().sequence_number))
|
|
||||||
.into_link(),
|
|
||||||
end: self
|
|
||||||
.query_args()
|
|
||||||
.pagination(Pagination::After(0))
|
|
||||||
.into_link(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let changes = &data
|
let (newer, older) = match self.before {
|
||||||
.into_iter()
|
Some(x) => (
|
||||||
.map(|x| Row {
|
Some(NavLinks {
|
||||||
resource: &self,
|
more: self.query_args().pagination(Pagination::After(x-1)).into_link(),
|
||||||
sequence_number: x.sequence_number,
|
end: self.query_args().pagination(Pagination::None).into_link(),
|
||||||
article_id: x.article_id,
|
}),
|
||||||
revision: x.revision,
|
extra_element.map(|_| NavLinks {
|
||||||
created: Local.from_utc_datetime(&x.created).to_rfc2822(),
|
more: self.query_args()
|
||||||
author: x.author,
|
.pagination(Pagination::Before(data.last().unwrap().sequence_number))
|
||||||
_slug: x.slug,
|
.into_link(),
|
||||||
title: x.title,
|
end: self.query_args().pagination(Pagination::After(0)).into_link(),
|
||||||
_latest: x.latest,
|
})
|
||||||
diff_link: if x.revision > 1 {
|
),
|
||||||
Some(format!(
|
None => (
|
||||||
"_diff/{}?{}",
|
None,
|
||||||
x.article_id,
|
extra_element.map(|_| NavLinks {
|
||||||
diff_resource::QueryParameters::new(
|
more: self.query_args()
|
||||||
x.revision as u32 - 1,
|
.pagination(Pagination::Before(data.last().unwrap().sequence_number))
|
||||||
x.revision as u32,
|
.into_link(),
|
||||||
)
|
end: self.query_args().pagination(Pagination::After(0)).into_link(),
|
||||||
))
|
}),
|
||||||
} else {
|
),
|
||||||
None
|
};
|
||||||
},
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(head.with_body(
|
let changes = &data.into_iter().map(|x| {
|
||||||
system_page(
|
Row {
|
||||||
None, // Hmm, should perhaps accept `base` as argument
|
|
||||||
"Changes",
|
|
||||||
Template {
|
|
||||||
resource: &self,
|
resource: &self,
|
||||||
show_authors: self.show_authors,
|
sequence_number: x.sequence_number,
|
||||||
newer,
|
article_id: x.article_id,
|
||||||
older,
|
revision: x.revision,
|
||||||
changes,
|
created: Local.from_utc_datetime(&x.created).to_rfc2822(),
|
||||||
},
|
author: x.author,
|
||||||
)
|
_slug: x.slug,
|
||||||
.to_string(),
|
title: x.title,
|
||||||
))
|
_latest: x.latest,
|
||||||
}))
|
diff_link:
|
||||||
|
if x.revision > 1 {
|
||||||
|
Some(format!("_diff/{}?{}",
|
||||||
|
x.article_id,
|
||||||
|
diff_resource::QueryParameters::new(
|
||||||
|
x.revision as u32 - 1,
|
||||||
|
x.revision as u32,
|
||||||
|
)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(head
|
||||||
|
.with_body(Layout {
|
||||||
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
|
title: "Changes",
|
||||||
|
body: &Template {
|
||||||
|
resource: &self,
|
||||||
|
show_authors: self.show_authors,
|
||||||
|
newer,
|
||||||
|
older,
|
||||||
|
changes
|
||||||
|
},
|
||||||
|
}.to_string()))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use futures::future::done;
|
use diff;
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use futures::future::done;
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_urlencoded;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::models::ArticleRevision;
|
use models::ArticleRevision;
|
||||||
use crate::site::Layout;
|
use site::Layout;
|
||||||
use crate::state::State;
|
use state::State;
|
||||||
use crate::theme;
|
use web::{Resource, ResponseFuture};
|
||||||
use crate::web::{Resource, ResponseFuture};
|
|
||||||
|
|
||||||
use super::changes_resource;
|
use super::changes_resource;
|
||||||
use super::pagination::Pagination;
|
use super::pagination::Pagination;
|
||||||
|
|
||||||
type BoxResource = Box<dyn Resource + Sync + Send>;
|
type BoxResource = Box<Resource + Sync + Send>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DiffLookup {
|
pub struct DiffLookup {
|
||||||
|
@ -46,28 +47,25 @@ impl DiffLookup {
|
||||||
Self { state }
|
Self { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(
|
pub fn lookup(&self, article_id: u32, query: Option<&str>) -> Box<Future<Item=Option<BoxResource>, Error=::web::Error>> {
|
||||||
&self,
|
|
||||||
article_id: u32,
|
|
||||||
query: Option<&str>,
|
|
||||||
) -> Box<dyn Future<Item = Option<BoxResource>, Error = crate::web::Error>> {
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
Box::new(
|
Box::new(done(
|
||||||
done(serde_urlencoded::from_str(query.unwrap_or("")).map_err(Into::into))
|
serde_urlencoded::from_str(query.unwrap_or(""))
|
||||||
.and_then(move |params: QueryParameters| {
|
.map_err(Into::into)
|
||||||
let from = state.get_article_revision(article_id as i32, params.from as i32);
|
).and_then(move |params: QueryParameters| {
|
||||||
let to = state.get_article_revision(article_id as i32, params.to as i32);
|
let from = state.get_article_revision(article_id as i32, params.from as i32);
|
||||||
|
let to = state.get_article_revision(article_id as i32, params.to as i32);
|
||||||
|
|
||||||
from.join(to)
|
from.join(to)
|
||||||
})
|
}).and_then(move |(from, to)| {
|
||||||
.and_then(move |(from, to)| match (from, to) {
|
match (from, to) {
|
||||||
(Some(from), Some(to)) => {
|
(Some(from), Some(to)) =>
|
||||||
Ok(Some(Box::new(DiffResource::new(from, to)) as BoxResource))
|
Ok(Some(Box::new(DiffResource::new(from, to)) as BoxResource)),
|
||||||
}
|
_ =>
|
||||||
_ => Ok(None),
|
Ok(None),
|
||||||
}),
|
}
|
||||||
)
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,10 +88,9 @@ impl Resource for DiffResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,8 +100,6 @@ impl Resource for DiffResource {
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
consecutive: bool,
|
consecutive: bool,
|
||||||
article_id: u32,
|
article_id: u32,
|
||||||
author: Option<&'a str>,
|
|
||||||
author_link: &'a str,
|
|
||||||
article_history_link: &'a str,
|
article_history_link: &'a str,
|
||||||
from_link: &'a str,
|
from_link: &'a str,
|
||||||
to_link: &'a str,
|
to_link: &'a str,
|
||||||
|
@ -121,88 +116,41 @@ impl Resource for DiffResource {
|
||||||
|
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(head.and_then(move |head| {
|
Box::new(head
|
||||||
let consecutive = self.to.revision - self.from.revision == 1;
|
.and_then(move |head| {
|
||||||
|
Ok(head
|
||||||
let author = match consecutive {
|
.with_body(Layout {
|
||||||
true => self.to.author.as_deref(),
|
base: Some("../"), // Hmm, should perhaps accept `base` as argument
|
||||||
false => None,
|
title: "Difference",
|
||||||
};
|
body: &Template {
|
||||||
|
consecutive: self.to.revision - self.from.revision == 1,
|
||||||
let author_link = &format!(
|
article_id: self.from.article_id as u32,
|
||||||
"_changes{}",
|
article_history_link: &format!("_changes{}",
|
||||||
changes_resource::QueryParameters::default()
|
changes_resource::QueryParameters::default()
|
||||||
.author(author.map(|x| x.to_owned()))
|
.article_id(Some(self.from.article_id))
|
||||||
.pagination(Pagination::After(self.from.sequence_number))
|
.pagination(Pagination::After(self.from.revision))
|
||||||
.into_link()
|
.into_link()
|
||||||
);
|
),
|
||||||
|
from_link: &format!("_revisions/{}/{}", self.from.article_id, self.from.revision),
|
||||||
let article_history_link = &format!(
|
to_link: &format!("_revisions/{}/{}", self.to.article_id, self.to.revision),
|
||||||
"_changes{}",
|
title: &diff::chars(&self.from.title, &self.to.title)
|
||||||
changes_resource::QueryParameters::default()
|
.into_iter()
|
||||||
.article_id(Some(self.from.article_id))
|
.map(|x| match x {
|
||||||
.pagination(Pagination::After(self.from.sequence_number))
|
diff::Result::Left(x) => Diff { removed: Some(x), ..Default::default() },
|
||||||
.into_link()
|
diff::Result::Both(x, _) => Diff { same: Some(x), ..Default::default() },
|
||||||
);
|
diff::Result::Right(x) => Diff { added: Some(x), ..Default::default() },
|
||||||
|
})
|
||||||
let title = &diff::chars(&self.from.title, &self.to.title)
|
.collect::<Vec<_>>(),
|
||||||
.into_iter()
|
lines: &diff::lines(&self.from.body, &self.to.body)
|
||||||
.map(|x| match x {
|
.into_iter()
|
||||||
diff::Result::Left(x) => Diff {
|
.map(|x| match x {
|
||||||
removed: Some(x),
|
diff::Result::Left(x) => Diff { removed: Some(x), ..Default::default() },
|
||||||
..Default::default()
|
diff::Result::Both(x, _) => Diff { same: Some(x), ..Default::default() },
|
||||||
},
|
diff::Result::Right(x) => Diff { added: Some(x), ..Default::default() },
|
||||||
diff::Result::Both(x, _) => Diff {
|
})
|
||||||
same: Some(x),
|
.collect::<Vec<_>>()
|
||||||
..Default::default()
|
},
|
||||||
},
|
}.to_string()))
|
||||||
diff::Result::Right(x) => Diff {
|
}))
|
||||||
added: Some(x),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let lines = &diff::lines(&self.from.body, &self.to.body)
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| match x {
|
|
||||||
diff::Result::Left(x) => Diff {
|
|
||||||
removed: Some(x),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
diff::Result::Both(x, _) => Diff {
|
|
||||||
same: Some(x),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
diff::Result::Right(x) => Diff {
|
|
||||||
added: Some(x),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(head.with_body(
|
|
||||||
Layout {
|
|
||||||
base: Some("../"), // Hmm, should perhaps accept `base` as argument
|
|
||||||
title: "Difference",
|
|
||||||
theme: theme::theme_from_str_hash("Difference"),
|
|
||||||
body: &Template {
|
|
||||||
consecutive,
|
|
||||||
article_id: self.from.article_id as u32,
|
|
||||||
author,
|
|
||||||
author_link,
|
|
||||||
article_history_link,
|
|
||||||
from_link: &format!(
|
|
||||||
"_revisions/{}/{}",
|
|
||||||
self.from.article_id, self.from.revision
|
|
||||||
),
|
|
||||||
to_link: &format!("_revisions/{}/{}", self.to.article_id, self.to.revision),
|
|
||||||
title,
|
|
||||||
lines,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
pub struct HtmlResource {
|
pub struct HtmlResource {
|
||||||
base: Option<&'static str>,
|
base: Option<&'static str>,
|
||||||
|
@ -15,14 +15,17 @@ pub struct HtmlResource {
|
||||||
|
|
||||||
impl HtmlResource {
|
impl HtmlResource {
|
||||||
pub fn new(base: Option<&'static str>, title: &'static str, html_body: &'static str) -> Self {
|
pub fn new(base: Option<&'static str>, title: &'static str, html_body: &'static str) -> Self {
|
||||||
HtmlResource {
|
HtmlResource { base, title, html_body }
|
||||||
base,
|
|
||||||
title,
|
|
||||||
html_body,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(BartDisplay)]
|
||||||
|
#[template="templates/simple.html"]
|
||||||
|
struct Template<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
html_body: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
impl Resource for HtmlResource {
|
impl Resource for HtmlResource {
|
||||||
fn allow(&self) -> Vec<hyper::Method> {
|
fn allow(&self) -> Vec<hyper::Method> {
|
||||||
use hyper::Method::*;
|
use hyper::Method::*;
|
||||||
|
@ -30,18 +33,26 @@ impl Resource for HtmlResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(head.and_then(move |head| {
|
Box::new(head
|
||||||
Ok(head.with_body(system_page(self.base, self.title, self.html_body).to_string()))
|
.and_then(move |head| {
|
||||||
}))
|
Ok(head
|
||||||
|
.with_body(Layout {
|
||||||
|
base: self.base,
|
||||||
|
title: self.title,
|
||||||
|
body: &Template {
|
||||||
|
title: self.title,
|
||||||
|
html_body: self.html_body,
|
||||||
|
},
|
||||||
|
}.to_string()))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
pub mod pagination;
|
pub mod pagination;
|
||||||
|
|
||||||
mod about_resource;
|
mod about_resource;
|
||||||
mod article_resource;
|
|
||||||
mod article_revision_resource;
|
mod article_revision_resource;
|
||||||
|
mod article_resource;
|
||||||
mod changes_resource;
|
mod changes_resource;
|
||||||
mod diff_resource;
|
mod diff_resource;
|
||||||
mod html_resource;
|
mod html_resource;
|
||||||
mod new_article_resource;
|
mod new_article_resource;
|
||||||
mod read_only_resource;
|
|
||||||
mod search_resource;
|
mod search_resource;
|
||||||
mod sitemap_resource;
|
mod sitemap_resource;
|
||||||
mod temporary_redirect_resource;
|
mod temporary_redirect_resource;
|
||||||
|
|
||||||
pub use self::about_resource::AboutResource;
|
pub use self::about_resource::AboutResource;
|
||||||
pub use self::article_resource::ArticleResource;
|
|
||||||
pub use self::article_revision_resource::ArticleRevisionResource;
|
pub use self::article_revision_resource::ArticleRevisionResource;
|
||||||
|
pub use self::article_resource::ArticleResource;
|
||||||
pub use self::changes_resource::{ChangesLookup, ChangesResource};
|
pub use self::changes_resource::{ChangesLookup, ChangesResource};
|
||||||
pub use self::diff_resource::{DiffLookup, DiffResource};
|
pub use self::diff_resource::{DiffLookup, DiffResource};
|
||||||
pub use self::html_resource::HtmlResource;
|
pub use self::html_resource::HtmlResource;
|
||||||
pub use self::new_article_resource::NewArticleResource;
|
pub use self::new_article_resource::NewArticleResource;
|
||||||
pub use self::read_only_resource::ReadOnlyResource;
|
|
||||||
pub use self::search_resource::SearchLookup;
|
pub use self::search_resource::SearchLookup;
|
||||||
pub use self::sitemap_resource::SitemapResource;
|
pub use self::sitemap_resource::SitemapResource;
|
||||||
pub use self::temporary_redirect_resource::TemporaryRedirectResource;
|
pub use self::temporary_redirect_resource::TemporaryRedirectResource;
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::{ContentType, Location};
|
use hyper::header::{ContentType, Location};
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_json;
|
||||||
|
use serde_urlencoded;
|
||||||
|
|
||||||
use crate::assets::ScriptJs;
|
use assets::ScriptJs;
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
use crate::site::Layout;
|
use site::Layout;
|
||||||
use crate::state::State;
|
use state::State;
|
||||||
use crate::theme::{self, Theme};
|
use web::{Resource, ResponseFuture};
|
||||||
use crate::web::{Resource, ResponseFuture};
|
|
||||||
|
|
||||||
const NEW: &str = "NEW";
|
const NEW: &str = "NEW";
|
||||||
|
|
||||||
|
@ -26,7 +27,6 @@ fn title_from_slug(slug: &str) -> String {
|
||||||
pub struct NewArticleResource {
|
pub struct NewArticleResource {
|
||||||
state: State,
|
state: State,
|
||||||
slug: Option<String>,
|
slug: Option<String>,
|
||||||
edit: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -34,12 +34,11 @@ struct CreateArticle {
|
||||||
base_revision: String,
|
base_revision: String,
|
||||||
title: String,
|
title: String,
|
||||||
body: String,
|
body: String,
|
||||||
theme: Option<Theme>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewArticleResource {
|
impl NewArticleResource {
|
||||||
pub fn new(state: State, slug: Option<String>, edit: bool) -> Self {
|
pub fn new(state: State, slug: Option<String>) -> Self {
|
||||||
Self { state, slug, edit }
|
Self { state, slug }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,22 +49,15 @@ impl Resource for NewArticleResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::NotFound)
|
||||||
.with_status(hyper::StatusCode::NotFound)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
// TODO Remove duplication with article_resource.rs:
|
|
||||||
struct SelectableTheme {
|
|
||||||
theme: Theme,
|
|
||||||
selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/article.html"]
|
#[template="templates/article.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
revision: &'a str,
|
revision: &'a str,
|
||||||
last_updated: Option<&'a str>,
|
last_updated: Option<&'a str>,
|
||||||
|
@ -75,56 +67,48 @@ impl Resource for NewArticleResource {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
raw: &'a str,
|
raw: &'a str,
|
||||||
rendered: &'a str,
|
rendered: &'a str,
|
||||||
themes: &'a [SelectableTheme],
|
|
||||||
}
|
}
|
||||||
impl<'a> Template<'a> {
|
impl<'a> Template<'a> {
|
||||||
fn script_js(&self) -> &'static str {
|
fn script_js_checksum(&self) -> &'static str {
|
||||||
ScriptJs::resource_name()
|
ScriptJs::checksum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = self
|
let title = self.slug.as_ref()
|
||||||
.slug
|
|
||||||
.as_ref()
|
|
||||||
.map_or("".to_owned(), |x| title_from_slug(x));
|
.map_or("".to_owned(), |x| title_from_slug(x));
|
||||||
|
|
||||||
Box::new(self.head().and_then(move |head| {
|
Box::new(self.head()
|
||||||
Ok(head.with_body(
|
.and_then(move |head| {
|
||||||
Layout {
|
Ok(head
|
||||||
base: None, // Hmm, should perhaps accept `base` as argument
|
.with_body(Layout {
|
||||||
title: &title,
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
theme: theme::Theme::Gray,
|
|
||||||
body: &Template {
|
|
||||||
revision: NEW,
|
|
||||||
last_updated: None,
|
|
||||||
edit: self.edit,
|
|
||||||
cancel_url: self.slug.as_deref(),
|
|
||||||
title: &title,
|
title: &title,
|
||||||
raw: "",
|
body: &Template {
|
||||||
rendered: EMPTY_ARTICLE_MESSAGE,
|
revision: NEW,
|
||||||
themes: &theme::THEMES
|
last_updated: None,
|
||||||
.iter()
|
|
||||||
.map(|&x| SelectableTheme {
|
// Implicitly start in edit-mode when no slug is given. This
|
||||||
theme: x,
|
// currently directly corresponds to the /_new endpoint
|
||||||
selected: false,
|
edit: self.slug.is_none(),
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
cancel_url: self.slug.as_ref().map(|x| &**x),
|
||||||
},
|
title: &title,
|
||||||
}
|
raw: "",
|
||||||
.to_string(),
|
rendered: EMPTY_ARTICLE_MESSAGE,
|
||||||
))
|
},
|
||||||
}))
|
}.to_string()))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
|
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
|
||||||
// TODO Check incoming Content-Type
|
// TODO Check incoming Content-Type
|
||||||
// TODO Refactor? Reduce duplication with ArticleResource::put?
|
// TODO Refactor? Reduce duplication with ArticleResource::put?
|
||||||
|
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{TimeZone, Local};
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/article_contents.html"]
|
#[template="templates/article_contents.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
rendered: String,
|
rendered: String,
|
||||||
|
@ -137,56 +121,45 @@ impl Resource for NewArticleResource {
|
||||||
revision: i32,
|
revision: i32,
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
body: &'a str,
|
body: &'a str,
|
||||||
theme: Theme,
|
|
||||||
rendered: &'a str,
|
rendered: &'a str,
|
||||||
last_updated: &'a str,
|
last_updated: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.concat2()
|
.concat2()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
|
.and_then(|body| {
|
||||||
.and_then(move |arg: CreateArticle| {
|
serde_urlencoded::from_bytes(&body)
|
||||||
if arg.base_revision != NEW {
|
.map_err(Into::into)
|
||||||
unimplemented!("Version update conflict");
|
})
|
||||||
}
|
.and_then(move |arg: CreateArticle| {
|
||||||
let theme = arg.theme.unwrap_or_else(theme::random);
|
if arg.base_revision != NEW {
|
||||||
self.state.create_article(
|
unimplemented!("Version update conflict");
|
||||||
self.slug.clone(),
|
}
|
||||||
arg.title,
|
self.state.create_article(self.slug.clone(), arg.title, arg.body, identity)
|
||||||
arg.body,
|
})
|
||||||
identity,
|
.and_then(|updated| {
|
||||||
theme,
|
futures::finished(Response::new()
|
||||||
)
|
.with_status(hyper::StatusCode::Ok)
|
||||||
})
|
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||||
.and_then(|updated| {
|
.with_body(serde_json::to_string(&PutResponse {
|
||||||
futures::finished(
|
slug: &updated.slug,
|
||||||
Response::new()
|
article_id: updated.article_id,
|
||||||
.with_status(hyper::StatusCode::Ok)
|
revision: updated.revision,
|
||||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
title: &updated.title,
|
||||||
.with_body(
|
body: &updated.body,
|
||||||
serde_json::to_string(&PutResponse {
|
rendered: &Template {
|
||||||
slug: &updated.slug,
|
title: &updated.title,
|
||||||
article_id: updated.article_id,
|
rendered: render_markdown(&updated.body),
|
||||||
revision: updated.revision,
|
}.to_string(),
|
||||||
title: &updated.title,
|
last_updated: &super::article_resource::last_updated(
|
||||||
body: &updated.body,
|
updated.article_id,
|
||||||
theme: updated.theme,
|
&Local.from_utc_datetime(&updated.created),
|
||||||
rendered: &Template {
|
updated.author.as_ref().map(|x| &**x)
|
||||||
title: &updated.title,
|
),
|
||||||
rendered: render_markdown(&updated.body),
|
}).expect("Should never fail"))
|
||||||
}
|
)
|
||||||
.to_string(),
|
})
|
||||||
last_updated: &super::article_resource::last_updated(
|
|
||||||
updated.article_id,
|
|
||||||
&Local.from_utc_datetime(&updated.created),
|
|
||||||
updated.author.as_deref(),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("Should never fail"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,32 +169,27 @@ impl Resource for NewArticleResource {
|
||||||
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.concat2()
|
.concat2()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
|
.and_then(|body| {
|
||||||
.and_then(move |arg: CreateArticle| {
|
serde_urlencoded::from_bytes(&body)
|
||||||
if arg.base_revision != NEW {
|
.map_err(Into::into)
|
||||||
unimplemented!("Version update conflict");
|
})
|
||||||
}
|
.and_then(move |arg: CreateArticle| {
|
||||||
let theme = arg.theme.unwrap_or_else(theme::random);
|
if arg.base_revision != NEW {
|
||||||
self.state.create_article(
|
unimplemented!("Version update conflict");
|
||||||
self.slug.clone(),
|
}
|
||||||
arg.title,
|
self.state.create_article(self.slug.clone(), arg.title, arg.body, identity)
|
||||||
arg.body,
|
})
|
||||||
identity,
|
.and_then(|updated| {
|
||||||
theme,
|
futures::finished(Response::new()
|
||||||
)
|
.with_status(hyper::StatusCode::SeeOther)
|
||||||
})
|
.with_header(ContentType(TEXT_PLAIN.clone()))
|
||||||
.and_then(|updated| {
|
.with_header(Location::new(updated.link().to_owned()))
|
||||||
futures::finished(
|
.with_body("See other")
|
||||||
Response::new()
|
)
|
||||||
.with_status(hyper::StatusCode::SeeOther)
|
})
|
||||||
.with_header(ContentType(TEXT_PLAIN.clone()))
|
|
||||||
.with_header(Location::new(updated.link().to_owned()))
|
|
||||||
.with_body("See other"),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,15 @@ pub struct Error;
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
write!(f, "`after` and `before` are mutually exclusive")
|
write!(f, "{}", (self as &error::Error).description())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl error::Error for Error {}
|
impl error::Error for Error {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"`after` and `before` are mutually exclusive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PaginationStruct<T> {
|
struct PaginationStruct<T> {
|
||||||
|
@ -33,16 +37,16 @@ impl<T> PaginationStruct<T> {
|
||||||
(Some(x), None) => Ok(Pagination::After(x)),
|
(Some(x), None) => Ok(Pagination::After(x)),
|
||||||
(None, Some(x)) => Ok(Pagination::Before(x)),
|
(None, Some(x)) => Ok(Pagination::Before(x)),
|
||||||
(None, None) => Ok(Pagination::None),
|
(None, None) => Ok(Pagination::None),
|
||||||
_ => Err(Error),
|
_ => Err(Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _from_str<'a, T: serde::Deserialize<'a>>(s: &'a str) -> Result<Pagination<T>, Error> {
|
pub fn _from_str<'a, T: serde::Deserialize<'a>>(s: &'a str) -> Result<Pagination<T>, Error> {
|
||||||
let pagination: PaginationStruct<T> = serde_urlencoded::from_str(s).map_err(|_| Error)?; // TODO Proper error reporting
|
let pagination: PaginationStruct<T> = serde_urlencoded::from_str(s).map_err(|_| Error)?; // TODO Proper error reporting
|
||||||
pagination.into_enum()
|
Ok(pagination.into_enum()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_fields<T>(after: Option<T>, before: Option<T>) -> Result<Pagination<T>, Error> {
|
pub fn from_fields<T>(after: Option<T>, before: Option<T>) -> Result<Pagination<T>, Error> {
|
||||||
PaginationStruct { after, before }.into_enum()
|
Ok(PaginationStruct { after, before }.into_enum()?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
use futures::Future;
|
|
||||||
use hyper::header::{CacheControl, CacheDirective, ContentLength, ContentType};
|
|
||||||
use hyper::server::*;
|
|
||||||
use hyper::StatusCode;
|
|
||||||
|
|
||||||
use crate::web::{Resource, ResponseFuture};
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub struct ReadOnlyResource {
|
|
||||||
pub content_type: ::hyper::mime::Mime,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resource for ReadOnlyResource {
|
|
||||||
fn allow(&self) -> Vec<::hyper::Method> {
|
|
||||||
use ::hyper::Method::*;
|
|
||||||
vec![Options, Head, Get]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
|
||||||
Box::new(::futures::finished(
|
|
||||||
Response::new()
|
|
||||||
.with_status(StatusCode::Ok)
|
|
||||||
.with_header(ContentType(self.content_type.clone()))
|
|
||||||
.with_header(CacheControl(vec![
|
|
||||||
CacheDirective::MustRevalidate,
|
|
||||||
CacheDirective::NoStore,
|
|
||||||
])),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
|
||||||
Box::new(self.head().map(move |head| {
|
|
||||||
head.with_header(ContentLength(self.body.len() as u64))
|
|
||||||
.with_body(self.body.clone())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,20 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::{Accept, ContentType};
|
use hyper::header::{Accept, ContentType};
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use serde_json;
|
||||||
|
use serde_urlencoded;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::models::SearchResult;
|
use models::SearchResult;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::state::State;
|
use state::State;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
const DEFAULT_LIMIT: u32 = 10;
|
const DEFAULT_LIMIT: u32 = 10;
|
||||||
const DEFAULT_SNIPPET_SIZE: u32 = 30;
|
const DEFAULT_SNIPPET_SIZE: u32 = 30;
|
||||||
|
|
||||||
type BoxResource = Box<dyn Resource + Sync + Send>;
|
type BoxResource = Box<Resource + Sync + Send>;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
pub struct QueryParameters {
|
pub struct QueryParameters {
|
||||||
|
@ -32,29 +34,21 @@ impl QueryParameters {
|
||||||
|
|
||||||
pub fn limit(self, limit: u32) -> Self {
|
pub fn limit(self, limit: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
limit: if limit != DEFAULT_LIMIT {
|
limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None },
|
||||||
Some(limit)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snippet_size(self, snippet_size: u32) -> Self {
|
pub fn snippet_size(self, snippet_size: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
snippet_size: if snippet_size != DEFAULT_SNIPPET_SIZE {
|
snippet_size: if snippet_size != DEFAULT_SNIPPET_SIZE { Some(snippet_size) } else { None },
|
||||||
Some(snippet_size)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_link(self) -> String {
|
pub fn into_link(self) -> String {
|
||||||
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
|
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
|
||||||
if !args.is_empty() {
|
if args.len() > 0 {
|
||||||
format!("_search?{}", args)
|
format!("_search?{}", args)
|
||||||
} else {
|
} else {
|
||||||
"_search".to_owned()
|
"_search".to_owned()
|
||||||
|
@ -72,16 +66,18 @@ impl SearchLookup {
|
||||||
Self { state }
|
Self { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(&self, query: Option<&str>) -> Result<Option<BoxResource>, crate::web::Error> {
|
pub fn lookup(&self, query: Option<&str>) -> Result<Option<BoxResource>, ::web::Error> {
|
||||||
let args: QueryParameters = serde_urlencoded::from_str(query.unwrap_or(""))?;
|
let args: QueryParameters = serde_urlencoded::from_str(query.unwrap_or(""))?;
|
||||||
|
|
||||||
Ok(Some(Box::new(SearchResource::new(
|
Ok(Some(Box::new(
|
||||||
self.state.clone(),
|
SearchResource::new(
|
||||||
args.q,
|
self.state.clone(),
|
||||||
args.limit.unwrap_or(DEFAULT_LIMIT),
|
args.q,
|
||||||
args.offset.unwrap_or(0),
|
args.limit.unwrap_or(DEFAULT_LIMIT),
|
||||||
args.snippet_size.unwrap_or(DEFAULT_SNIPPET_SIZE),
|
args.offset.unwrap_or(0),
|
||||||
))))
|
args.snippet_size.unwrap_or(DEFAULT_SNIPPET_SIZE),
|
||||||
|
)
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,21 +98,8 @@ pub enum ResponseType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchResource {
|
impl SearchResource {
|
||||||
pub fn new(
|
pub fn new(state: State, query: Option<String>, limit: u32, offset: u32, snippet_size: u32) -> Self {
|
||||||
state: State,
|
Self { state, response_type: ResponseType::Html, query, limit, offset, snippet_size }
|
||||||
query: Option<String>,
|
|
||||||
limit: u32,
|
|
||||||
offset: u32,
|
|
||||||
snippet_size: u32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
response_type: ResponseType::Html,
|
|
||||||
query,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
snippet_size,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_args(&self) -> QueryParameters {
|
fn query_args(&self) -> QueryParameters {
|
||||||
|
@ -124,9 +107,9 @@ impl SearchResource {
|
||||||
q: self.query.clone(),
|
q: self.query.clone(),
|
||||||
..QueryParameters::default()
|
..QueryParameters::default()
|
||||||
}
|
}
|
||||||
.offset(self.offset)
|
.offset(self.offset)
|
||||||
.limit(self.limit)
|
.limit(self.limit)
|
||||||
.snippet_size(self.snippet_size)
|
.snippet_size(self.snippet_size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,24 +126,21 @@ impl Resource for SearchResource {
|
||||||
|
|
||||||
self.response_type = match accept.first() {
|
self.response_type = match accept.first() {
|
||||||
Some(&QualityItem { item: ref mime, .. })
|
Some(&QualityItem { item: ref mime, .. })
|
||||||
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON =>
|
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON
|
||||||
{
|
=> ResponseType::Json,
|
||||||
ResponseType::Json
|
|
||||||
}
|
|
||||||
_ => ResponseType::Html,
|
_ => ResponseType::Html,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
let content_type = match self.response_type {
|
let content_type = match &self.response_type {
|
||||||
ResponseType::Json => ContentType(APPLICATION_JSON.clone()),
|
&ResponseType::Json => ContentType(APPLICATION_JSON.clone()),
|
||||||
ResponseType::Html => ContentType(TEXT_HTML.clone()),
|
&ResponseType::Html => ContentType(TEXT_HTML.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(content_type)
|
||||||
.with_header(content_type),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +154,7 @@ impl Resource for SearchResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/search.html"]
|
#[template="templates/search.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
query: &'a str,
|
query: &'a str,
|
||||||
hits: &'a [(usize, &'a SearchResult)],
|
hits: &'a [(usize, &'a SearchResult)],
|
||||||
|
@ -183,66 +163,55 @@ impl Resource for SearchResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Show a search "front page" when no query is given:
|
// TODO: Show a search "front page" when no query is given:
|
||||||
let query = self
|
let query = self.query.as_ref().map(|x| x.clone()).unwrap_or("".to_owned());
|
||||||
.query
|
|
||||||
.as_ref()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "".to_owned());
|
|
||||||
|
|
||||||
let data = self.state.search_query(
|
let data = self.state.search_query(query, (self.limit + 1) as i32, self.offset as i32, self.snippet_size as i32);
|
||||||
query,
|
|
||||||
(self.limit + 1) as i32,
|
|
||||||
self.offset as i32,
|
|
||||||
self.snippet_size as i32,
|
|
||||||
);
|
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(data.join(head).and_then(move |(mut data, head)| {
|
Box::new(data.join(head)
|
||||||
let prev = if self.offset > 0 {
|
.and_then(move |(mut data, head)| {
|
||||||
Some(
|
let prev = if self.offset > 0 {
|
||||||
self.query_args()
|
Some(self.query_args()
|
||||||
.offset(self.offset.saturating_sub(self.limit))
|
.offset(self.offset.saturating_sub(self.limit))
|
||||||
.into_link(),
|
.into_link()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let next = if data.len() > self.limit as usize {
|
let next = if data.len() > self.limit as usize {
|
||||||
data.pop();
|
data.pop();
|
||||||
Some(
|
Some(self.query_args()
|
||||||
self.query_args()
|
|
||||||
.offset(self.offset + self.limit)
|
.offset(self.offset + self.limit)
|
||||||
.into_link(),
|
.into_link()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.response_type {
|
match &self.response_type {
|
||||||
ResponseType::Json => Ok(head.with_body(
|
&ResponseType::Json => Ok(head
|
||||||
serde_json::to_string(&JsonResponse {
|
.with_body(serde_json::to_string(&JsonResponse {
|
||||||
query: self.query.as_deref().unwrap_or(""),
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
hits: &data,
|
hits: &data,
|
||||||
prev,
|
|
||||||
next,
|
|
||||||
})
|
|
||||||
.expect("Should never fail"),
|
|
||||||
)),
|
|
||||||
ResponseType::Html => Ok(head.with_body(
|
|
||||||
system_page(
|
|
||||||
None, // Hmm, should perhaps accept `base` as argument
|
|
||||||
"Search",
|
|
||||||
&Template {
|
|
||||||
query: self.query.as_deref().unwrap_or(""),
|
|
||||||
hits: &data.iter().enumerate().collect::<Vec<_>>(),
|
|
||||||
prev,
|
prev,
|
||||||
next,
|
next,
|
||||||
},
|
}).expect("Should never fail"))
|
||||||
)
|
),
|
||||||
.to_string(),
|
&ResponseType::Html => Ok(head
|
||||||
)),
|
.with_body(Layout {
|
||||||
}
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
}))
|
title: "Search",
|
||||||
|
body: &Template {
|
||||||
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
|
hits: &data.iter()
|
||||||
|
.enumerate()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
},
|
||||||
|
}.to_string())),
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use crate::mimes::*;
|
use mimes::*;
|
||||||
use crate::models::ArticleRevisionStub;
|
use models::ArticleRevisionStub;
|
||||||
use crate::site::system_page;
|
use site::Layout;
|
||||||
use crate::state::State;
|
use state::State;
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
pub struct SitemapResource {
|
pub struct SitemapResource {
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -26,16 +26,15 @@ impl Resource for SitemapResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::Ok)
|
||||||
.with_status(hyper::StatusCode::Ok)
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_header(ContentType(TEXT_HTML.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
#[template = "templates/sitemap.html"]
|
#[template="templates/sitemap.html"]
|
||||||
struct Template<'a> {
|
struct Template<'a> {
|
||||||
articles: &'a [ArticleRevisionStub],
|
articles: &'a [ArticleRevisionStub],
|
||||||
}
|
}
|
||||||
|
@ -43,17 +42,16 @@ impl Resource for SitemapResource {
|
||||||
let data = self.state.get_latest_article_revision_stubs();
|
let data = self.state.get_latest_article_revision_stubs();
|
||||||
let head = self.head();
|
let head = self.head();
|
||||||
|
|
||||||
Box::new(data.join(head).and_then(move |(articles, head)| {
|
Box::new(data.join(head)
|
||||||
Ok(head.with_body(
|
.and_then(move |(articles, head)| {
|
||||||
system_page(
|
Ok(head
|
||||||
None, // Hmm, should perhaps accept `base` as argument
|
.with_body(Layout {
|
||||||
"Sitemap",
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
Template {
|
title: "Sitemap",
|
||||||
articles: &articles,
|
body: &Template {
|
||||||
},
|
articles: &articles,
|
||||||
)
|
},
|
||||||
.to_string(),
|
}.to_string()))
|
||||||
))
|
}))
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
use hyper;
|
||||||
use hyper::header::Location;
|
use hyper::header::Location;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use crate::web::{Resource, ResponseFuture};
|
use web::{Resource, ResponseFuture};
|
||||||
|
|
||||||
pub struct TemporaryRedirectResource {
|
pub struct TemporaryRedirectResource {
|
||||||
location: String,
|
location: String,
|
||||||
|
@ -14,17 +14,14 @@ impl TemporaryRedirectResource {
|
||||||
Self { location }
|
Self { location }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_slug<S: AsRef<str>>(slug: S, edit: bool) -> Self {
|
pub fn from_slug<S: AsRef<str>>(slug: S) -> Self {
|
||||||
let base = if slug.as_ref().is_empty() {
|
|
||||||
"."
|
|
||||||
} else {
|
|
||||||
slug.as_ref()
|
|
||||||
};
|
|
||||||
|
|
||||||
let tail = if edit { "?edit" } else { "" };
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
location: format!("{}{}", base, tail),
|
location:
|
||||||
|
if slug.as_ref().is_empty() {
|
||||||
|
"."
|
||||||
|
} else {
|
||||||
|
slug.as_ref()
|
||||||
|
}.to_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,18 +33,18 @@ impl Resource for TemporaryRedirectResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> ResponseFuture {
|
fn head(&self) -> ResponseFuture {
|
||||||
Box::new(futures::finished(
|
Box::new(futures::finished(Response::new()
|
||||||
Response::new()
|
.with_status(hyper::StatusCode::TemporaryRedirect)
|
||||||
.with_status(hyper::StatusCode::TemporaryRedirect)
|
.with_header(Location::new(self.location.clone()))
|
||||||
.with_header(Location::new(self.location.clone())),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
Box::new(
|
Box::new(self.head()
|
||||||
self.head()
|
.and_then(move |head| {
|
||||||
.and_then(move |head| Ok(head.with_body(format!("Moved to {}", self.location)))),
|
Ok(head
|
||||||
)
|
.with_body(format!("Moved to {}", self.location)))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put(self: Box<Self>, _body: hyper::Body, _identity: Option<String>) -> ResponseFuture {
|
fn put(self: Box<Self>, _body: hyper::Body, _identity: Option<String>) -> ResponseFuture {
|
||||||
|
|
128
src/site.rs
128
src/site.rs
|
@ -4,20 +4,20 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use futures::{self, Future};
|
use futures::{self, Future};
|
||||||
|
|
||||||
use hyper::header::{Accept, ContentType, Server};
|
use hyper::header::{Accept, ContentType, Server};
|
||||||
use hyper::mime;
|
use hyper::mime;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
use hyper;
|
||||||
|
|
||||||
use crate::assets::{SearchJs, StyleCss, ThemesCss};
|
use assets::{StyleCss, SearchJs};
|
||||||
use crate::build_config;
|
use build_config;
|
||||||
use crate::theme;
|
use web::Lookup;
|
||||||
use crate::web::Lookup;
|
use wiki_lookup::WikiLookup;
|
||||||
use crate::wiki_lookup::WikiLookup;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
|
static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
|
||||||
static ref SERVER: Server = Server::new(build_config::HTTP_SERVER.as_str());
|
static ref SERVER: Server =
|
||||||
|
Server::new(build_config::HTTP_SERVER.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
header! { (XIdentity, "X-Identity") => [String] }
|
header! { (XIdentity, "X-Identity") => [String] }
|
||||||
|
@ -27,53 +27,15 @@ header! { (XIdentity, "X-Identity") => [String] }
|
||||||
pub struct Layout<'a, T: 'a + fmt::Display> {
|
pub struct Layout<'a, T: 'a + fmt::Display> {
|
||||||
pub base: Option<&'a str>,
|
pub base: Option<&'a str>,
|
||||||
pub title: &'a str,
|
pub title: &'a str,
|
||||||
pub theme: theme::Theme,
|
pub body: &'a T,
|
||||||
pub body: T,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: 'a + fmt::Display> Layout<'a, T> {
|
impl<'a, T: 'a + fmt::Display> Layout<'a, T> {
|
||||||
pub fn themes_css(&self) -> &str {
|
pub fn style_css_checksum(&self) -> &str { StyleCss::checksum() }
|
||||||
ThemesCss::resource_name()
|
pub fn search_js_checksum(&self) -> &str { SearchJs::checksum() }
|
||||||
}
|
|
||||||
pub fn style_css(&self) -> &str {
|
|
||||||
StyleCss::resource_name()
|
|
||||||
}
|
|
||||||
pub fn search_js(&self) -> &str {
|
|
||||||
SearchJs::resource_name()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn project_name(&self) -> &str {
|
pub fn project_name(&self) -> &str { build_config::PROJECT_NAME }
|
||||||
build_config::PROJECT_NAME
|
pub fn version(&self) -> &str { build_config::VERSION.as_str() }
|
||||||
}
|
|
||||||
pub fn version(&self) -> &str {
|
|
||||||
build_config::VERSION.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
|
||||||
#[template = "templates/system_page_layout.html"]
|
|
||||||
pub struct SystemPageLayout<'a, T: 'a + fmt::Display> {
|
|
||||||
title: &'a str,
|
|
||||||
html_body: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn system_page<'a, T>(
|
|
||||||
base: Option<&'a str>,
|
|
||||||
title: &'a str,
|
|
||||||
body: T,
|
|
||||||
) -> Layout<'a, SystemPageLayout<'a, T>>
|
|
||||||
where
|
|
||||||
T: 'a + fmt::Display,
|
|
||||||
{
|
|
||||||
Layout {
|
|
||||||
base,
|
|
||||||
title,
|
|
||||||
theme: theme::theme_from_str_hash(title),
|
|
||||||
body: SystemPageLayout {
|
|
||||||
title,
|
|
||||||
html_body: body,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(BartDisplay)]
|
#[derive(BartDisplay)]
|
||||||
|
@ -91,39 +53,41 @@ pub struct Site {
|
||||||
|
|
||||||
impl Site {
|
impl Site {
|
||||||
pub fn new(root: WikiLookup, trust_identity: bool) -> Site {
|
pub fn new(root: WikiLookup, trust_identity: bool) -> Site {
|
||||||
Site {
|
Site { root, trust_identity }
|
||||||
root,
|
|
||||||
trust_identity,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found(base: Option<&str>) -> Response {
|
fn not_found(base: Option<&str>) -> Response {
|
||||||
Response::new()
|
Response::new()
|
||||||
.with_header(ContentType(TEXT_HTML.clone()))
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_body(system_page(base, "Not found", NotFound).to_string())
|
.with_body(Layout {
|
||||||
|
base: base,
|
||||||
|
title: "Not found",
|
||||||
|
body: &NotFound,
|
||||||
|
}.to_string())
|
||||||
.with_status(hyper::StatusCode::NotFound)
|
.with_status(hyper::StatusCode::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal_server_error(
|
fn internal_server_error(base: Option<&str>, err: Box<::std::error::Error + Send + Sync>) -> Response {
|
||||||
base: Option<&str>,
|
|
||||||
err: Box<dyn ::std::error::Error + Send + Sync>,
|
|
||||||
) -> Response {
|
|
||||||
eprintln!("Internal Server Error:\n{:#?}", err);
|
eprintln!("Internal Server Error:\n{:#?}", err);
|
||||||
|
|
||||||
Response::new()
|
Response::new()
|
||||||
.with_header(ContentType(TEXT_HTML.clone()))
|
.with_header(ContentType(TEXT_HTML.clone()))
|
||||||
.with_body(system_page(base, "Internal server error", InternalServerError).to_string())
|
.with_body(Layout {
|
||||||
|
base,
|
||||||
|
title: "Internal server error",
|
||||||
|
body: &InternalServerError,
|
||||||
|
}.to_string())
|
||||||
.with_status(hyper::StatusCode::InternalServerError)
|
.with_status(hyper::StatusCode::InternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root_base_from_request_uri(path: &str) -> Option<String> {
|
fn root_base_from_request_uri(path: &str) -> Option<String> {
|
||||||
assert!(path.starts_with('/'));
|
assert!(path.starts_with("/"));
|
||||||
let slashes = path[1..].matches('/').count();
|
let slashes = path[1..].matches('/').count();
|
||||||
|
|
||||||
match slashes {
|
match slashes {
|
||||||
0 => None,
|
0 => None,
|
||||||
n => Some("../".repeat(n)),
|
n => Some(::std::iter::repeat("../").take(n).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +95,7 @@ impl Service for Site {
|
||||||
type Request = Request;
|
type Request = Request;
|
||||||
type Response = Response;
|
type Response = Response;
|
||||||
type Error = hyper::Error;
|
type Error = hyper::Error;
|
||||||
type Future = Box<dyn futures::Future<Item = Response, Error = Self::Error>>;
|
type Future = Box<futures::Future<Item = Response, Error = Self::Error>>;
|
||||||
|
|
||||||
fn call(&self, req: Request) -> Self::Future {
|
fn call(&self, req: Request) -> Self::Future {
|
||||||
let (method, uri, _http_version, headers, body) = req.deconstruct();
|
let (method, uri, _http_version, headers, body) = req.deconstruct();
|
||||||
|
@ -143,31 +107,29 @@ impl Service for Site {
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let accept_header = headers.get().cloned().unwrap_or_else(|| Accept(vec![]));
|
let accept_header = headers.get().map(|x: &Accept| x.clone()).unwrap_or(Accept(vec![]));
|
||||||
|
|
||||||
let base = root_base_from_request_uri(uri.path());
|
let base = root_base_from_request_uri(uri.path());
|
||||||
let base2 = base.clone(); // Bah, stupid clone
|
let base2 = base.clone(); // Bah, stupid clone
|
||||||
|
|
||||||
Box::new(
|
Box::new(self.root.lookup(uri.path(), uri.query())
|
||||||
self.root
|
.and_then(move |resource| match resource {
|
||||||
.lookup(uri.path(), uri.query())
|
Some(mut resource) => {
|
||||||
.and_then(move |resource| match resource {
|
use hyper::Method::*;
|
||||||
Some(mut resource) => {
|
resource.hacky_inject_accept_header(accept_header);
|
||||||
use hyper::Method::*;
|
match method {
|
||||||
resource.hacky_inject_accept_header(accept_header);
|
Options => Box::new(futures::finished(resource.options())),
|
||||||
match method {
|
Head => resource.head(),
|
||||||
Options => Box::new(futures::finished(resource.options())),
|
Get => resource.get(),
|
||||||
Head => resource.head(),
|
Put => resource.put(body, identity),
|
||||||
Get => resource.get(),
|
Post => resource.post(body, identity),
|
||||||
Put => resource.put(body, identity),
|
_ => Box::new(futures::finished(resource.method_not_allowed()))
|
||||||
Post => resource.post(body, identity),
|
|
||||||
_ => Box::new(futures::finished(resource.method_not_allowed())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => Box::new(futures::finished(Self::not_found(base.as_deref()))),
|
},
|
||||||
})
|
None => Box::new(futures::finished(Self::not_found(base.as_ref().map(|x| &**x))))
|
||||||
.or_else(move |err| Ok(Self::internal_server_error(base2.as_deref(), err)))
|
})
|
||||||
.map(|response| response.with_header(SERVER.clone())),
|
.or_else(move |err| Ok(Self::internal_server_error(base2.as_ref().map(|x| &**x), err)))
|
||||||
|
.map(|response| response.with_header(SERVER.clone()))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
667
src/state.rs
667
src/state.rs
File diff suppressed because it is too large
Load diff
232
src/theme.rs
232
src/theme.rs
|
@ -1,232 +0,0 @@
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use diesel::backend::Backend;
|
|
||||||
use diesel::deserialize::{self, FromSql};
|
|
||||||
use diesel::serialize::{self, Output, ToSql};
|
|
||||||
use diesel::sql_types::Text;
|
|
||||||
use diesel::sqlite::Sqlite;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] // Serde
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
#[derive(AsExpression, FromSqlRow)] // Diesel
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
pub enum Theme {
|
|
||||||
Red,
|
|
||||||
Pink,
|
|
||||||
Purple,
|
|
||||||
DeepPurple,
|
|
||||||
Indigo,
|
|
||||||
Blue,
|
|
||||||
LightBlue,
|
|
||||||
Cyan,
|
|
||||||
Teal,
|
|
||||||
Green,
|
|
||||||
LightGreen,
|
|
||||||
Lime,
|
|
||||||
Yellow,
|
|
||||||
Amber,
|
|
||||||
Orange,
|
|
||||||
DeepOrange,
|
|
||||||
Brown,
|
|
||||||
Gray,
|
|
||||||
BlueGray,
|
|
||||||
}
|
|
||||||
|
|
||||||
use self::Theme::*;
|
|
||||||
|
|
||||||
forward_display_to_serde!(Theme);
|
|
||||||
forward_from_str_to_serde!(Theme);
|
|
||||||
|
|
||||||
pub const THEMES: [Theme; 19] = [
|
|
||||||
Red, Pink, Purple, DeepPurple, Indigo, Blue, LightBlue, Cyan, Teal, Green, LightGreen, Lime,
|
|
||||||
Yellow, Amber, Orange, DeepOrange, Brown, Gray, BlueGray,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn theme_from_str_hash(x: &str) -> Theme {
|
|
||||||
let hash = seahash::hash(x.as_bytes()) as usize;
|
|
||||||
let choice = hash % THEMES.len();
|
|
||||||
THEMES[choice]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn random() -> Theme {
|
|
||||||
use rand::Rng;
|
|
||||||
*rand::thread_rng()
|
|
||||||
.choose(&THEMES)
|
|
||||||
.expect("Could only fail for an empty slice")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToSql<Text, Sqlite> for Theme {
|
|
||||||
fn to_sql<W: Write>(&self, out: &mut Output<W, Sqlite>) -> serialize::Result {
|
|
||||||
ToSql::<Text, Sqlite>::to_sql(&self.to_string(), out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql<Text, Sqlite> for Theme {
|
|
||||||
fn from_sql(value: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
|
|
||||||
// See Diesel's documentation on how to implement FromSql for Sqlite,
|
|
||||||
// especially with regards to the unsafe conversion below.
|
|
||||||
// http://docs.diesel.rs/diesel/deserialize/trait.FromSql.html
|
|
||||||
let text_ptr = <*const str as FromSql<Text, Sqlite>>::from_sql(value)?;
|
|
||||||
let text = unsafe { &*text_ptr };
|
|
||||||
text.parse().map_err(Into::into)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CssClass(Theme);
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn css_class(self) -> CssClass {
|
|
||||||
CssClass(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
|
|
||||||
impl Display for CssClass {
|
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
|
|
||||||
write!(fmt, "theme-{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sql_query;
|
|
||||||
use diesel::sql_types::Text;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_serialize() {
|
|
||||||
assert_eq!(serde_plain::to_string(&Theme::Red).unwrap(), "red");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize_kebab_case() {
|
|
||||||
assert_eq!(
|
|
||||||
serde_plain::to_string(&Theme::LightGreen).unwrap(),
|
|
||||||
"light-green"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize_json() {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Test {
|
|
||||||
x: Theme,
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_string(&Test { x: Theme::Red }).unwrap(),
|
|
||||||
"{\"x\":\"red\"}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_json() {
|
|
||||||
#[derive(Deserialize, Debug, PartialEq, Eq)]
|
|
||||||
struct Test {
|
|
||||||
x: Theme,
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::from_str::<Test>("{\"x\":\"red\"}").unwrap(),
|
|
||||||
Test { x: Theme::Red }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize_urlencoded() {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Test {
|
|
||||||
x: Theme,
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
serde_urlencoded::to_string(&Test { x: Theme::Red }).unwrap(),
|
|
||||||
"x=red"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_urlencoded() {
|
|
||||||
#[derive(Deserialize, Debug, PartialEq, Eq)]
|
|
||||||
struct Test {
|
|
||||||
x: Theme,
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
serde_urlencoded::from_str::<Test>("x=red").unwrap(),
|
|
||||||
Test { x: Theme::Red }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_display() {
|
|
||||||
assert_eq!(&Theme::Red.to_string(), "red");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_kebab_case() {
|
|
||||||
assert_eq!(&Theme::LightGreen.to_string(), "light-green");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_from_str() {
|
|
||||||
let indigo: Theme = "indigo".parse().unwrap();
|
|
||||||
assert_eq!(indigo, Theme::Indigo);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_number() {
|
|
||||||
assert_eq!(Theme::Red as i32, 0);
|
|
||||||
assert_eq!(Theme::LightGreen as i32, 10);
|
|
||||||
assert_eq!(Theme::BlueGray as i32, 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_str_hash() {
|
|
||||||
assert_eq!(theme_from_str_hash("Bartefjes"), Theme::Orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn css_class_display() {
|
|
||||||
assert_eq!(&Theme::Red.css_class().to_string(), "theme-red");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_db_roundtrip() -> Result<(), Box<dyn Error>> {
|
|
||||||
let conn = SqliteConnection::establish(":memory:")?;
|
|
||||||
|
|
||||||
#[derive(QueryableByName, PartialEq, Eq, Debug)]
|
|
||||||
struct Row {
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = sql_query("SELECT ? as theme")
|
|
||||||
.bind::<Text, _>(DeepPurple)
|
|
||||||
.load::<Row>(&conn)?;
|
|
||||||
|
|
||||||
assert_eq!(&[Row { theme: DeepPurple }], res.as_slice());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn db_invalid_value_gives_error() -> Result<(), Box<dyn Error>> {
|
|
||||||
let conn = SqliteConnection::establish(":memory:")?;
|
|
||||||
|
|
||||||
#[derive(QueryableByName, PartialEq, Eq, Debug)]
|
|
||||||
struct Row {
|
|
||||||
#[sql_type = "Text"]
|
|
||||||
theme: Theme,
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = sql_query("SELECT 'green' as theme").load::<Row>(&conn);
|
|
||||||
assert!(res.is_ok());
|
|
||||||
|
|
||||||
let res = sql_query("SELECT 'blueish-yellow' as theme").load::<Row>(&conn);
|
|
||||||
assert!(res.is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
use futures;
|
||||||
|
|
||||||
pub trait Lookup {
|
pub trait Lookup {
|
||||||
type Resource;
|
type Resource;
|
||||||
type Error;
|
type Error;
|
||||||
type Future: futures::Future<Item = Option<Self::Resource>, Error = Self::Error>;
|
type Future: futures::Future<Item=Option<Self::Resource>, Error=Self::Error>;
|
||||||
|
|
||||||
fn lookup(&self, path: &str, query: Option<&str>) -> Self::Future;
|
fn lookup(&self, path: &str, query: Option<&str>) -> Self::Future;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod lookup;
|
|
||||||
mod resource;
|
mod resource;
|
||||||
|
mod lookup;
|
||||||
|
|
||||||
pub use self::lookup::*;
|
|
||||||
pub use self::resource::*;
|
pub use self::resource::*;
|
||||||
|
pub use self::lookup::*;
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
|
use futures;
|
||||||
use futures::{Future, Stream};
|
use futures::{Future, Stream};
|
||||||
use hyper::server::Response;
|
|
||||||
use hyper::{self, header, mime, server};
|
use hyper::{self, header, mime, server};
|
||||||
|
use hyper::server::Response;
|
||||||
|
use std;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap();
|
static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
pub type Error = Box<std::error::Error + Send + Sync>;
|
||||||
pub type ResponseFuture = Box<dyn futures::Future<Item = server::Response, Error = Error>>;
|
pub type ResponseFuture = Box<futures::Future<Item = server::Response, Error = Error>>;
|
||||||
|
|
||||||
pub trait Resource {
|
pub trait Resource {
|
||||||
fn allow(&self) -> Vec<hyper::Method>;
|
fn allow(&self) -> Vec<hyper::Method>;
|
||||||
|
@ -21,24 +23,22 @@ pub trait Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
|
fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
|
||||||
where
|
where Self: 'static
|
||||||
Self: 'static,
|
|
||||||
{
|
{
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
|
.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(move |_| futures::finished(self.method_not_allowed())),
|
.and_then(move |_| futures::finished(self.method_not_allowed()))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
|
fn post(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
|
||||||
where
|
where Self: 'static
|
||||||
Self: 'static,
|
|
||||||
{
|
{
|
||||||
Box::new(
|
Box::new(body
|
||||||
body.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
|
.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.and_then(move |_| futures::finished(self.method_not_allowed())),
|
.and_then(move |_| futures::finished(self.method_not_allowed()))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,36 +2,48 @@ use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
|
|
||||||
|
use futures::{Future, finished, failed, done};
|
||||||
use futures::future::FutureResult;
|
use futures::future::FutureResult;
|
||||||
use futures::{done, failed, finished, Future};
|
|
||||||
use percent_encoding::percent_decode;
|
use percent_encoding::percent_decode;
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
|
|
||||||
use crate::resources::*;
|
use resources::*;
|
||||||
use crate::state::State;
|
use assets::*;
|
||||||
use crate::web::{Lookup, Resource};
|
use state::State;
|
||||||
|
use web::{Lookup, Resource};
|
||||||
|
|
||||||
#[allow(unused)]
|
type BoxResource = Box<Resource + Sync + Send>;
|
||||||
use crate::assets::*;
|
type ResourceFn = Box<Fn() -> BoxResource + Sync + Send>;
|
||||||
|
|
||||||
type BoxResource = Box<dyn Resource + Sync + Send>;
|
|
||||||
type ResourceFn = Box<dyn Fn() -> BoxResource + Sync + Send>;
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref LICENSES_MAP: HashMap<&'static str, ResourceFn> = hashmap! {
|
static ref ASSETS_MAP: HashMap<String, ResourceFn> = hashmap!{
|
||||||
"bsd-3-clause" => Box::new(|| Box::new(
|
format!("style-{}.css", StyleCss::checksum()) =>
|
||||||
|
Box::new(|| Box::new(StyleCss) as BoxResource) as ResourceFn,
|
||||||
|
|
||||||
|
format!("script-{}.js", ScriptJs::checksum()) =>
|
||||||
|
Box::new(|| Box::new(ScriptJs) as BoxResource) as ResourceFn,
|
||||||
|
|
||||||
|
format!("search-{}.js", SearchJs::checksum()) =>
|
||||||
|
Box::new(|| Box::new(SearchJs) as BoxResource) as ResourceFn,
|
||||||
|
|
||||||
|
format!("amatic-sc-v9-latin-regular.woff") =>
|
||||||
|
Box::new(|| Box::new(AmaticFont) as BoxResource) as ResourceFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
static ref LICENSES_MAP: HashMap<String, ResourceFn> = hashmap!{
|
||||||
|
"bsd-3-clause".to_owned() => Box::new(|| Box::new(
|
||||||
HtmlResource::new(Some("../"), "The 3-Clause BSD License", include_str!("licenses/bsd-3-clause.html"))
|
HtmlResource::new(Some("../"), "The 3-Clause BSD License", include_str!("licenses/bsd-3-clause.html"))
|
||||||
) as BoxResource) as ResourceFn,
|
) as BoxResource) as ResourceFn,
|
||||||
"gpl3" => Box::new(|| Box::new(
|
"gpl3".to_owned() => Box::new(|| Box::new(
|
||||||
HtmlResource::new(Some("../"), "GNU General Public License", include_str!("licenses/gpl3.html"))
|
HtmlResource::new(Some("../"), "GNU General Public License", include_str!("licenses/gpl3.html"))
|
||||||
) as BoxResource) as ResourceFn,
|
) as BoxResource) as ResourceFn,
|
||||||
"mit" => Box::new(|| Box::new(
|
"mit".to_owned() => Box::new(|| Box::new(
|
||||||
HtmlResource::new(Some("../"), "The MIT License", include_str!("licenses/mit.html"))
|
HtmlResource::new(Some("../"), "The MIT License", include_str!("licenses/mit.html"))
|
||||||
) as BoxResource) as ResourceFn,
|
) as BoxResource) as ResourceFn,
|
||||||
"mpl2" => Box::new(|| Box::new(
|
"mpl2".to_owned() => Box::new(|| Box::new(
|
||||||
HtmlResource::new(Some("../"), "Mozilla Public License Version 2.0", include_str!("licenses/mpl2.html"))
|
HtmlResource::new(Some("../"), "Mozilla Public License Version 2.0", include_str!("licenses/mpl2.html"))
|
||||||
) as BoxResource) as ResourceFn,
|
) as BoxResource) as ResourceFn,
|
||||||
"sil-ofl-1.1" => Box::new(|| Box::new(
|
"sil-ofl-1.1".to_owned() => Box::new(|| Box::new(
|
||||||
HtmlResource::new(Some("../"), "SIL Open Font License", include_str!("licenses/sil-ofl-1.1.html"))
|
HtmlResource::new(Some("../"), "SIL Open Font License", include_str!("licenses/sil-ofl-1.1.html"))
|
||||||
) as BoxResource) as ResourceFn,
|
) as BoxResource) as ResourceFn,
|
||||||
};
|
};
|
||||||
|
@ -54,10 +66,9 @@ fn split_one(path: &str) -> Result<(Cow<str>, Option<&str>), Utf8Error> {
|
||||||
Ok((head, tail))
|
Ok((head, tail))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_lookup(
|
fn map_lookup(map: &HashMap<String, ResourceFn>, path: &str) ->
|
||||||
map: &HashMap<&str, ResourceFn>,
|
FutureResult<Option<BoxResource>, Box<::std::error::Error + Send + Sync>>
|
||||||
path: &str,
|
{
|
||||||
) -> FutureResult<Option<BoxResource>, Box<dyn ::std::error::Error + Send + Sync>> {
|
|
||||||
let (head, tail) = match split_one(path) {
|
let (head, tail) = match split_one(path) {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(x) => return failed(x.into()),
|
Err(x) => return failed(x.into()),
|
||||||
|
@ -73,49 +84,13 @@ fn map_lookup(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
fn fs_lookup(
|
|
||||||
root: &str,
|
|
||||||
path: &str,
|
|
||||||
) -> FutureResult<Option<BoxResource>, Box<dyn ::std::error::Error + Send + Sync>> {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::prelude::*;
|
|
||||||
|
|
||||||
let extension = path.rsplit_once('.').map(|x| x.1);
|
|
||||||
|
|
||||||
let content_type = match extension {
|
|
||||||
Some("html") => "text/html",
|
|
||||||
Some("css") => "text/css",
|
|
||||||
Some("js") => "application/javascript",
|
|
||||||
Some("woff") => "application/font-woff",
|
|
||||||
_ => "application/binary",
|
|
||||||
}
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut filename = root.to_string();
|
|
||||||
filename.push_str(path);
|
|
||||||
|
|
||||||
let mut f = File::open(&filename).unwrap_or_else(|_| panic!("Not found: {}", filename));
|
|
||||||
|
|
||||||
let mut body = Vec::new();
|
|
||||||
f.read_to_end(&mut body).expect("Unable to read file");
|
|
||||||
|
|
||||||
finished(Some(Box::new(ReadOnlyResource { content_type, body })))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WikiLookup {
|
impl WikiLookup {
|
||||||
pub fn new(state: State, show_authors: bool) -> WikiLookup {
|
pub fn new(state: State, show_authors: bool) -> WikiLookup {
|
||||||
let changes_lookup = ChangesLookup::new(state.clone(), show_authors);
|
let changes_lookup = ChangesLookup::new(state.clone(), show_authors);
|
||||||
let diff_lookup = DiffLookup::new(state.clone());
|
let diff_lookup = DiffLookup::new(state.clone());
|
||||||
let search_lookup = SearchLookup::new(state.clone());
|
let search_lookup = SearchLookup::new(state.clone());
|
||||||
|
|
||||||
WikiLookup {
|
WikiLookup { state, changes_lookup, diff_lookup, search_lookup }
|
||||||
state,
|
|
||||||
changes_lookup,
|
|
||||||
diff_lookup,
|
|
||||||
search_lookup,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn revisions_lookup(&self, path: &str, _query: Option<&str>) -> <Self as Lookup>::Future {
|
fn revisions_lookup(&self, path: &str, _query: Option<&str>) -> <Self as Lookup>::Future {
|
||||||
|
@ -133,12 +108,12 @@ impl WikiLookup {
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::new(
|
Box::new(
|
||||||
self.state
|
self.state.get_article_revision(article_id, revision)
|
||||||
.get_article_revision(article_id, revision)
|
.and_then(|article_revision|
|
||||||
.and_then(|article_revision| {
|
Ok(article_revision.map(move |x| Box::new(
|
||||||
Ok(article_revision
|
ArticleRevisionResource::new(x)
|
||||||
.map(move |x| Box::new(ArticleRevisionResource::new(x)) as BoxResource))
|
) as BoxResource))
|
||||||
}),
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +130,14 @@ impl WikiLookup {
|
||||||
Err(_) => return Box::new(finished(None)),
|
Err(_) => return Box::new(finished(None)),
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::new(self.state.get_article_slug(article_id).and_then(|slug| {
|
Box::new(
|
||||||
Ok(slug.map(|slug| {
|
self.state.get_article_slug(article_id)
|
||||||
Box::new(TemporaryRedirectResource::new(format!("../{}", slug))) as BoxResource
|
.and_then(|slug|
|
||||||
}))
|
Ok(slug.map(|slug| Box::new(
|
||||||
}))
|
TemporaryRedirectResource::new(format!("../{}", slug))
|
||||||
|
) as BoxResource))
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_lookup_f(&self, path: &str, query: Option<&str>) -> <Self as Lookup>::Future {
|
fn diff_lookup_f(&self, path: &str, query: Option<&str>) -> <Self as Lookup>::Future {
|
||||||
|
@ -185,30 +163,26 @@ impl WikiLookup {
|
||||||
};
|
};
|
||||||
|
|
||||||
match (head.as_ref(), tail) {
|
match (head.as_ref(), tail) {
|
||||||
("_about", None) => Box::new(finished(Some(
|
("_about", None) =>
|
||||||
Box::new(AboutResource::new()) as BoxResource
|
Box::new(finished(Some(Box::new(AboutResource::new()) as BoxResource))),
|
||||||
))),
|
("_about", Some(license)) =>
|
||||||
("_about", Some(license)) => Box::new(map_lookup(&LICENSES_MAP, license)),
|
Box::new(map_lookup(&LICENSES_MAP, license)),
|
||||||
#[cfg(feature = "dynamic-assets")]
|
("_assets", Some(asset)) =>
|
||||||
("_assets", Some(asset)) => Box::new(fs_lookup(
|
Box::new(map_lookup(&ASSETS_MAP, asset)),
|
||||||
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/"),
|
("_by_id", Some(tail)) =>
|
||||||
asset,
|
self.by_id_lookup(tail, query),
|
||||||
)),
|
("_changes", None) =>
|
||||||
#[cfg(not(feature = "dynamic-assets"))]
|
Box::new(self.changes_lookup.lookup(query)),
|
||||||
("_assets", Some(asset)) => Box::new(map_lookup(&ASSETS_MAP, asset)),
|
("_diff", Some(tail)) =>
|
||||||
("_by_id", Some(tail)) => self.by_id_lookup(tail, query),
|
self.diff_lookup_f(tail, query),
|
||||||
("_changes", None) => Box::new(self.changes_lookup.lookup(query)),
|
("_new", None) =>
|
||||||
("_diff", Some(tail)) => self.diff_lookup_f(tail, query),
|
Box::new(finished(Some(Box::new(NewArticleResource::new(self.state.clone(), None)) as BoxResource))),
|
||||||
("_new", None) => Box::new(finished(Some(Box::new(NewArticleResource::new(
|
("_revisions", Some(tail)) =>
|
||||||
self.state.clone(),
|
self.revisions_lookup(tail, query),
|
||||||
None,
|
("_search", None) =>
|
||||||
true,
|
Box::new(done(self.search_lookup.lookup(query))),
|
||||||
)) as BoxResource))),
|
("_sitemap", None) =>
|
||||||
("_revisions", Some(tail)) => self.revisions_lookup(tail, query),
|
Box::new(finished(Some(Box::new(SitemapResource::new(self.state.clone())) as BoxResource))),
|
||||||
("_search", None) => Box::new(done(self.search_lookup.lookup(query))),
|
|
||||||
("_sitemap", None) => Box::new(finished(Some(Box::new(SitemapResource::new(
|
|
||||||
self.state.clone(),
|
|
||||||
)) as BoxResource))),
|
|
||||||
_ => Box::new(finished(None)),
|
_ => Box::new(finished(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,49 +198,42 @@ impl WikiLookup {
|
||||||
return Box::new(finished(None));
|
return Box::new(finished(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit = query == Some("edit");
|
|
||||||
|
|
||||||
// Normalize all user-generated slugs:
|
// Normalize all user-generated slugs:
|
||||||
let slugified_slug = slugify(&slug);
|
let slugified_slug = slugify(&slug);
|
||||||
if slugified_slug != slug {
|
if slugified_slug != slug {
|
||||||
return Box::new(finished(Some(
|
return Box::new(finished(Some(
|
||||||
Box::new(TemporaryRedirectResource::from_slug(slugified_slug, edit)) as BoxResource,
|
Box::new(TemporaryRedirectResource::from_slug(slugified_slug)) as BoxResource
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
let edit = query == Some("edit");
|
||||||
let slug = slug.into_owned();
|
let slug = slug.into_owned();
|
||||||
|
|
||||||
use crate::state::SlugLookup;
|
use state::SlugLookup;
|
||||||
Box::new(self.state.lookup_slug(slug.clone()).and_then(move |x| {
|
Box::new(self.state.lookup_slug(slug.clone())
|
||||||
Ok(Some(match x {
|
.and_then(move |x| Ok(Some(match x {
|
||||||
SlugLookup::Miss => {
|
SlugLookup::Miss =>
|
||||||
Box::new(NewArticleResource::new(state, Some(slug), edit)) as BoxResource
|
Box::new(NewArticleResource::new(state, Some(slug))) as BoxResource,
|
||||||
}
|
SlugLookup::Hit { article_id, revision } =>
|
||||||
SlugLookup::Hit {
|
Box::new(ArticleResource::new(state, article_id, revision, edit)) as BoxResource,
|
||||||
article_id,
|
SlugLookup::Redirect(slug) =>
|
||||||
revision,
|
Box::new(TemporaryRedirectResource::from_slug(slug)) as BoxResource,
|
||||||
} => {
|
})))
|
||||||
Box::new(ArticleResource::new(state, article_id, revision, edit)) as BoxResource
|
)
|
||||||
}
|
|
||||||
SlugLookup::Redirect(slug) => {
|
|
||||||
Box::new(TemporaryRedirectResource::from_slug(slug, edit)) as BoxResource
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Lookup for WikiLookup {
|
impl Lookup for WikiLookup {
|
||||||
type Resource = BoxResource;
|
type Resource = BoxResource;
|
||||||
type Error = Box<dyn ::std::error::Error + Send + Sync>;
|
type Error = Box<::std::error::Error + Send + Sync>;
|
||||||
type Future = Box<dyn Future<Item = Option<Self::Resource>, Error = Self::Error>>;
|
type Future = Box<Future<Item = Option<Self::Resource>, Error = Self::Error>>;
|
||||||
|
|
||||||
fn lookup(&self, path: &str, query: Option<&str>) -> Self::Future {
|
fn lookup(&self, path: &str, query: Option<&str>) -> Self::Future {
|
||||||
assert!(path.starts_with('/'));
|
assert!(path.starts_with("/"));
|
||||||
let path = &path[1..];
|
let path = &path[1..];
|
||||||
|
|
||||||
if path.starts_with('_') {
|
if path.starts_with("_") {
|
||||||
self.reserved_lookup(path, query)
|
self.reserved_lookup(path, query)
|
||||||
} else {
|
} else {
|
||||||
self.article_lookup(path, query)
|
self.article_lookup(path, query)
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>About Sausagewiki</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
<p>This site is running Sausagewiki, a simple, self-contained wiki engine,
|
<p>This site is running Sausagewiki, a simple, self-contained wiki engine,
|
||||||
version {{version()}}.</p>
|
version {{version()}}.</p>
|
||||||
<p>Copyright © 2017 Magnus Hovland Hoff.</p>
|
<p>Copyright © 2017 Magnus Hovland Hoff.</p>
|
||||||
|
@ -14,9 +20,9 @@ See also <a href="_about/gpl3">the full license text</a> and the
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Without <a href="https://www.rust-lang.org/en-US/">Rust</a> and
|
A huge thanks to <a href="https://www.rust-lang.org/en-US/">Rust</a> and
|
||||||
<a href="https://www.sqlite.org/">SQLite</a>, Sausagewiki would never have
|
<a href="https://www.sqlite.org/">SQLite</a>. Without these amazing projects,
|
||||||
materialized. Huge thanks to the creators. Another big thanks for the support,
|
Sausagewiki would never have materialized. Another big thanks for the support,
|
||||||
discussions and testing by the amazing developers at Revolverhuset.
|
discussions and testing by the amazing developers at Revolverhuset.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -33,3 +39,7 @@ copyright holders and distributed under various licenses:
|
||||||
{{/deps}}
|
{{/deps}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>footer/default.html}}
|
||||||
|
|
|
@ -1,24 +1,16 @@
|
||||||
<script src="_assets/{{script_js()}}" defer></script>
|
<script src="_assets/script-{{script_js_checksum()}}.js" defer></script>
|
||||||
|
|
||||||
<div class="container {{#edit?}}edit{{/edit}}">
|
<div class="container {{#edit?}}edit{{/edit}}">
|
||||||
<div class="rendered">
|
<div class="rendered">
|
||||||
{{>article_contents.html}}
|
{{>article_contents.html}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form autocomplete="off" id="article-editor" action="" method="POST">
|
|
||||||
|
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<div class="hero">
|
<form action="" method="POST">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title" required></h1>
|
<h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title" required></h1>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="theme-picker">
|
|
||||||
{{#themes}}
|
|
||||||
<input autocomplete="off" type="radio" name="theme" value="{{.theme}}"{{#.selected?}} checked{{/.selected}} class="theme-picker--option {{.theme.css_class()}} themed">
|
|
||||||
{{/themes}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<p>
|
<p>
|
||||||
|
@ -28,26 +20,15 @@
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-controls">
|
<div class="editor-controls">
|
||||||
{{#edit?}}
|
{{#cancel_url}}
|
||||||
<div class="cancel-interaction-group {{#cancel_url}}interaction-group--root--enabled{{/cancel_url}}{{^cancel_url}}interaction-group--root--disabled{{/cancel_url}}">
|
<a class="cancel" href="{{.}}">Cancel</a>
|
||||||
<a class="interaction-group--enabled button button-cancel cancel" href="{{#cancel_url}}{{.}}{{/cancel_url}}">Cancel</a>
|
{{/cancel_url}}
|
||||||
<button class="interaction-group--disabled button button-cancel" disabled>Cancel</a>
|
<button type=submit>Save</button>
|
||||||
</div>
|
|
||||||
<button class="button button-default" type=submit {{^edit?}}disabled{{/edit}}>Save</button>
|
|
||||||
{{/edit}}
|
|
||||||
{{^edit?}}
|
|
||||||
<div class="cancel-interaction-group interaction-group--root--disabled">
|
|
||||||
<a class="interaction-group--enabled button button-cancel cancel" href="{{#cancel_url}}{{.}}{{/cancel_url}}">Cancel</a>
|
|
||||||
<button class="interaction-group--disabled button button-cancel" disabled>Cancel</a>
|
|
||||||
</div>
|
|
||||||
<button class="button button-default" type=submit disabled>Save</button>
|
|
||||||
{{/edit}}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<div class="hero">
|
|
||||||
<header>
|
<header>
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
{{{rendered}}}
|
{{{rendered}}}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
<div class="notice">
|
<div class="notice">
|
||||||
<p>
|
<p>
|
||||||
You are viewing an historical version of <a href="{{link_current}}">this article</a>,
|
You are viewing an historical version of <a href="{{link_current}}">this article</a>,
|
||||||
|
@ -9,4 +11,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{{rendered}}}
|
<div class="rendered">
|
||||||
|
{{>article_contents.html}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
{{>footer/items.html}}
|
||||||
|
</footer>
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Changes</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
<p>
|
<p>
|
||||||
These are the {{^newer}}most recent{{/newer}} changes
|
These are the {{^newer}}most recent{{/newer}} changes
|
||||||
made to{{{subject_clause()}}}{{#author()}} by {{.}}{{/author()}}.
|
made to{{{subject_clause()}}}{{#author()}} by {{.}}{{/author()}}.
|
||||||
|
@ -38,3 +44,7 @@
|
||||||
><li><a rel="last" href="{{.end}}">First changes</a></li
|
><li><a rel="last" href="{{.end}}">First changes</a></li
|
||||||
></ul></nav>{{/older}}
|
></ul></nav>{{/older}}
|
||||||
{{#changes?}}{{^older}}<p>There are no older changes.</p>{{/older}}{{/changes}}
|
{{#changes?}}{{^older}}<p>There are no older changes.</p>{{/older}}{{/changes}}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>footer/default.html}}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<div class="hero">
|
|
||||||
<header>
|
|
||||||
<h1>{{#title}}{{#.removed}}<span class="removed">{{.}}</span>{{/.removed}}{{#.same}}{{.}}{{/.same}}{{#.added}}<span class="added">{{.}}</span>{{/.added}}{{/title}}</h1>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="notice">
|
<div class="notice">
|
||||||
<p>
|
<p>
|
||||||
You are viewing the difference between two {{#consecutive?}}consecutive{{/consecutive}}
|
You are viewing the difference between two {{#consecutive?}}consecutive{{/consecutive}}
|
||||||
revisions of <a href="_by_id/{{article_id}}">this article</a>.
|
revisions of <a href="_by_id/{{article_id}}">this article</a>.
|
||||||
{{#author}}This changeset was authored by <a href="{{..author_link}}">{{.}}</a>.{{/author}}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -19,6 +12,10 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>{{#title}}{{#.removed}}<span class="removed">{{.}}</span>{{/.removed}}{{#.same}}{{.}}{{/.same}}{{#.added}}<span class="added">{{.}}</span>{{/.added}}{{/title}}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<pre class="diff">{{#lines}}{{#.removed}}<span class="removed">{{.}}
|
<pre class="diff">{{#lines}}{{#.removed}}<span class="removed">{{.}}
|
||||||
</span>{{/.removed}}{{#.same}}<span class="same">{{.}}
|
</span>{{/.removed}}{{#.same}}<span class="same">{{.}}
|
||||||
|
|
|
@ -1 +1,11 @@
|
||||||
<p>This page was not found.</p>
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<p>This page was not found.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>../footer/default.html}}
|
||||||
|
|
|
@ -1 +1,11 @@
|
||||||
<p>An error has occurred.</p>
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Internal server error</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<p>An error has occurred.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>../footer/default.html}}
|
||||||
|
|
|
@ -3,13 +3,12 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{{title}}</title>
|
<title>{{title}}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="utf-8">
|
|
||||||
{{#base}}<base href="{{.}}">{{/base}}
|
{{#base}}<base href="{{.}}">{{/base}}
|
||||||
<link href="_assets/{{themes_css()}}" rel="stylesheet">
|
<link rel=preload href="_assets/amatic-sc-v9-latin-regular.woff" as=font type="font/woff" crossorigin=anonymous>
|
||||||
<link href="_assets/{{style_css()}}" rel="stylesheet">
|
<link href="_assets/style-{{style_css_checksum()}}.css" rel="stylesheet">
|
||||||
<meta name="generator" content="{{project_name()}} {{version()}}" />
|
<meta name="generator" content="{{project_name()}} {{version()}}" />
|
||||||
</head>
|
</head>
|
||||||
<body class="{{theme.css_class()}}">
|
<body>
|
||||||
{{>search_input.html}}
|
{{>search_input.html}}
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Search</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
{{#hits?}}
|
{{#hits?}}
|
||||||
<p>Search results for the query <b>{{query}}</b>:</p>
|
<p>Search results for the query <b>{{query}}</b>:</p>
|
||||||
|
|
||||||
|
@ -19,3 +25,8 @@
|
||||||
{{^hits?}}
|
{{^hits?}}
|
||||||
<p>Your search for <b>{{query}}</b> gave no results.</p>
|
<p>Your search for <b>{{query}}</b> gave no results.</p>
|
||||||
{{/hits}}
|
{{/hits}}
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>footer/default.html}}
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
<div class="search-container">
|
|
||||||
<form class="search keyboard-focus-control" action=_search method=GET>
|
<form class="search keyboard-focus-control" action=_search method=GET>
|
||||||
<div class="search-widget-container">
|
<input data-focusindex="0" type=search name=q placeholder=search autocomplete=off>
|
||||||
<input data-focusindex="0" type=search name=q placeholder=Search autocomplete=off>
|
<ul class="live-results search-results">
|
||||||
<ul class="live-results search-results">
|
</ul>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<ul id="search-result-prototype" class="prototype"><li class="search-result"><a tabindex="0" class="link" href=""><span class="title"></span> – <span class="snippet"></span></a></li></ul>
|
<ul id="search-result-prototype" class="prototype"><li class="search-result"><a tabindex="0" class="link" href=""><span class="title"></span> – <span class="snippet"></span></a></li></ul>
|
||||||
<script src="_assets/{{search_js()}}" defer></script>
|
<script src="_assets/search-{{search_js_checksum()}}.js" defer></script>
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero">
|
|
||||||
<header>
|
<header>
|
||||||
<h1>{{title}}</h1>
|
<h1>{{title}}</h1>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
{{{html_body}}}
|
{{{html_body}}}
|
|
@ -1,5 +1,15 @@
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Sitemap</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article>
|
||||||
<ul class="dense"
|
<ul class="dense"
|
||||||
{{#articles}}
|
{{#articles}}
|
||||||
><li><a href="{{.link()}}">{{.title}}</a></li
|
><li><a href="{{.link()}}">{{.title}}</a></li
|
||||||
{{/articles}}
|
{{/articles}}
|
||||||
></ul>
|
></ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{>footer/default.html}}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json, colorsys
|
|
||||||
|
|
||||||
colors = json.load(open("material-colors.json", "r"))
|
|
||||||
palettes = colors['palettes']
|
|
||||||
|
|
||||||
def hex_to_rgb(h):
|
|
||||||
return tuple(int(h[i:i+2], 16) / 255 for i in (0, 2 ,4))
|
|
||||||
|
|
||||||
def to_linear(x):
|
|
||||||
if x < 0.04045:
|
|
||||||
return x / 12.92
|
|
||||||
else:
|
|
||||||
return pow((x + 0.055) / 1.055, 2.4)
|
|
||||||
|
|
||||||
def rgb_to_linear(rgb):
|
|
||||||
return [to_linear(x) for x in rgb]
|
|
||||||
|
|
||||||
def luma(rgb):
|
|
||||||
r, g, b = rgb_to_linear(rgb)
|
|
||||||
return (0.2126*r + 0.7152*g + 0.0722*b)
|
|
||||||
|
|
||||||
def prep(x):
|
|
||||||
cols = x['colors']
|
|
||||||
rgb = [hex_to_rgb(c[1:]) for c in cols]
|
|
||||||
brightness = [luma(c) for c in rgb]
|
|
||||||
hue = [colorsys.rgb_to_hsv(*c)[0] * 360 for c in rgb]
|
|
||||||
sat = [colorsys.rgb_to_hsv(*c)[1] for c in rgb]
|
|
||||||
|
|
||||||
main_index = 5
|
|
||||||
if brightness[main_index] >= 0.4:
|
|
||||||
main_index = 6
|
|
||||||
|
|
||||||
dark_main = brightness[main_index] < 0.5
|
|
||||||
|
|
||||||
input_index = main_index + (-2 if dark_main else 1)
|
|
||||||
|
|
||||||
h = hue[main_index]
|
|
||||||
s = sat[main_index]
|
|
||||||
|
|
||||||
alt = blues
|
|
||||||
if s > 0.3 and (h < 40 or h >= 300):
|
|
||||||
alt = yellows
|
|
||||||
|
|
||||||
return {
|
|
||||||
"name": x['shade'].lower().replace(' ', '-'),
|
|
||||||
"main": cols[main_index],
|
|
||||||
"input": cols[input_index],
|
|
||||||
"text": "white",
|
|
||||||
"link": alt[2 if dark_main else 7],
|
|
||||||
}
|
|
||||||
|
|
||||||
blues = [x for x in palettes if x['shade'] == "Blue"][0]["colors"]
|
|
||||||
yellows = [x for x in palettes if x['shade'] == "Yellow"][0]["colors"]
|
|
||||||
|
|
||||||
themes = [prep(palette) for palette in palettes]
|
|
||||||
|
|
||||||
print(
|
|
||||||
"\n".join(
|
|
||||||
"\
|
|
||||||
.theme-{name} {{\n\
|
|
||||||
--theme-main: {main};\n\
|
|
||||||
--theme-text: {text};\n\
|
|
||||||
--theme-input: {input};\n\
|
|
||||||
--theme-link: {link};\n\
|
|
||||||
}}\n".format(**x)
|
|
||||||
for x in themes
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
# print("[" + ', '.join('"'+x['name']+'"' for x in themes) + "]")
|
|
|
@ -1,369 +0,0 @@
|
||||||
{
|
|
||||||
"names": [
|
|
||||||
"50",
|
|
||||||
"100",
|
|
||||||
"200",
|
|
||||||
"300",
|
|
||||||
"400",
|
|
||||||
"500",
|
|
||||||
"600",
|
|
||||||
"700",
|
|
||||||
"800",
|
|
||||||
"900",
|
|
||||||
"A100",
|
|
||||||
"A200",
|
|
||||||
"A400",
|
|
||||||
"A700"
|
|
||||||
],
|
|
||||||
"palettes": [
|
|
||||||
{
|
|
||||||
"shade": "Red",
|
|
||||||
"colors": [
|
|
||||||
"#FFEBEE",
|
|
||||||
"#FFCDD2",
|
|
||||||
"#EF9A9A",
|
|
||||||
"#E57373",
|
|
||||||
"#EF5350",
|
|
||||||
"#F44336",
|
|
||||||
"#E53935",
|
|
||||||
"#D32F2F",
|
|
||||||
"#C62828",
|
|
||||||
"#B71C1C",
|
|
||||||
"#FF8A80",
|
|
||||||
"#FF5252",
|
|
||||||
"#FF1744",
|
|
||||||
"#D50000"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Pink",
|
|
||||||
"colors": [
|
|
||||||
"#FCE4EC",
|
|
||||||
"#F8BBD0",
|
|
||||||
"#F48FB1",
|
|
||||||
"#F06292",
|
|
||||||
"#EC407A",
|
|
||||||
"#E91E63",
|
|
||||||
"#D81B60",
|
|
||||||
"#C2185B",
|
|
||||||
"#AD1457",
|
|
||||||
"#880E4F",
|
|
||||||
"#FF80AB",
|
|
||||||
"#FF4081",
|
|
||||||
"#F50057",
|
|
||||||
"#C51162"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Purple",
|
|
||||||
"colors": [
|
|
||||||
"#F3E5F5",
|
|
||||||
"#E1BEE7",
|
|
||||||
"#CE93D8",
|
|
||||||
"#BA68C8",
|
|
||||||
"#AB47BC",
|
|
||||||
"#9C27B0",
|
|
||||||
"#8E24AA",
|
|
||||||
"#7B1FA2",
|
|
||||||
"#6A1B9A",
|
|
||||||
"#4A148C",
|
|
||||||
"#EA80FC",
|
|
||||||
"#E040FB",
|
|
||||||
"#D500F9",
|
|
||||||
"#AA00FF"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Deep purple",
|
|
||||||
"colors": [
|
|
||||||
"#EDE7F6",
|
|
||||||
"#D1C4E9",
|
|
||||||
"#B39DDB",
|
|
||||||
"#9575CD",
|
|
||||||
"#7E57C2",
|
|
||||||
"#673AB7",
|
|
||||||
"#5E35B1",
|
|
||||||
"#512DA8",
|
|
||||||
"#4527A0",
|
|
||||||
"#311B92",
|
|
||||||
"#B388FF",
|
|
||||||
"#7C4DFF",
|
|
||||||
"#651FFF",
|
|
||||||
"#6200EA"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Indigo",
|
|
||||||
"colors": [
|
|
||||||
"#E8EAF6",
|
|
||||||
"#C5CAE9",
|
|
||||||
"#9FA8DA",
|
|
||||||
"#7986CB",
|
|
||||||
"#5C6BC0",
|
|
||||||
"#3F51B5",
|
|
||||||
"#3949AB",
|
|
||||||
"#303F9F",
|
|
||||||
"#283593",
|
|
||||||
"#1A237E",
|
|
||||||
"#8C9EFF",
|
|
||||||
"#536DFE",
|
|
||||||
"#3D5AFE",
|
|
||||||
"#304FFE"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Blue",
|
|
||||||
"colors": [
|
|
||||||
"#E3F2FD",
|
|
||||||
"#BBDEFB",
|
|
||||||
"#90CAF9",
|
|
||||||
"#64B5F6",
|
|
||||||
"#42A5F5",
|
|
||||||
"#2196F3",
|
|
||||||
"#1E88E5",
|
|
||||||
"#1976D2",
|
|
||||||
"#1565C0",
|
|
||||||
"#0D47A1",
|
|
||||||
"#82B1FF",
|
|
||||||
"#448AFF",
|
|
||||||
"#2979FF",
|
|
||||||
"#2962FF"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Light Blue",
|
|
||||||
"colors": [
|
|
||||||
"#E1F5FE",
|
|
||||||
"#B3E5FC",
|
|
||||||
"#81D4FA",
|
|
||||||
"#4FC3F7",
|
|
||||||
"#29B6F6",
|
|
||||||
"#03A9F4",
|
|
||||||
"#039BE5",
|
|
||||||
"#0288D1",
|
|
||||||
"#0277BD",
|
|
||||||
"#01579B",
|
|
||||||
"#80D8FF",
|
|
||||||
"#40C4FF",
|
|
||||||
"#00B0FF",
|
|
||||||
"#0091EA"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Cyan",
|
|
||||||
"colors": [
|
|
||||||
"#E0F7FA",
|
|
||||||
"#B2EBF2",
|
|
||||||
"#80DEEA",
|
|
||||||
"#4DD0E1",
|
|
||||||
"#26C6DA",
|
|
||||||
"#00BCD4",
|
|
||||||
"#00ACC1",
|
|
||||||
"#0097A7",
|
|
||||||
"#00838F",
|
|
||||||
"#006064",
|
|
||||||
"#84FFFF",
|
|
||||||
"#18FFFF",
|
|
||||||
"#00E5FF",
|
|
||||||
"#00B8D4"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Teal",
|
|
||||||
"colors": [
|
|
||||||
"#E0F2F1",
|
|
||||||
"#B2DFDB",
|
|
||||||
"#80CBC4",
|
|
||||||
"#4DB6AC",
|
|
||||||
"#26A69A",
|
|
||||||
"#009688",
|
|
||||||
"#00897B",
|
|
||||||
"#00796B",
|
|
||||||
"#00695C",
|
|
||||||
"#004D40",
|
|
||||||
"#A7FFEB",
|
|
||||||
"#64FFDA",
|
|
||||||
"#1DE9B6",
|
|
||||||
"#00BFA5"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Green",
|
|
||||||
"colors": [
|
|
||||||
"#E8F5E9",
|
|
||||||
"#C8E6C9",
|
|
||||||
"#A5D6A7",
|
|
||||||
"#81C784",
|
|
||||||
"#66BB6A",
|
|
||||||
"#4CAF50",
|
|
||||||
"#43A047",
|
|
||||||
"#388E3C",
|
|
||||||
"#2E7D32",
|
|
||||||
"#1B5E20",
|
|
||||||
"#B9F6CA",
|
|
||||||
"#69F0AE",
|
|
||||||
"#00E676",
|
|
||||||
"#00C853"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Light Green",
|
|
||||||
"colors": [
|
|
||||||
"#F1F8E9",
|
|
||||||
"#DCEDC8",
|
|
||||||
"#C5E1A5",
|
|
||||||
"#AED581",
|
|
||||||
"#9CCC65",
|
|
||||||
"#8BC34A",
|
|
||||||
"#7CB342",
|
|
||||||
"#689F38",
|
|
||||||
"#558B2F",
|
|
||||||
"#33691E",
|
|
||||||
"#CCFF90",
|
|
||||||
"#B2FF59",
|
|
||||||
"#76FF03",
|
|
||||||
"#64DD17"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Lime",
|
|
||||||
"colors": [
|
|
||||||
"#F9FBE7",
|
|
||||||
"#F0F4C3",
|
|
||||||
"#E6EE9C",
|
|
||||||
"#DCE775",
|
|
||||||
"#D4E157",
|
|
||||||
"#CDDC39",
|
|
||||||
"#C0CA33",
|
|
||||||
"#AFB42B",
|
|
||||||
"#9E9D24",
|
|
||||||
"#827717",
|
|
||||||
"#F4FF81",
|
|
||||||
"#EEFF41",
|
|
||||||
"#C6FF00",
|
|
||||||
"#AEEA00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Yellow",
|
|
||||||
"colors": [
|
|
||||||
"#FFFDE7",
|
|
||||||
"#FFF9C4",
|
|
||||||
"#FFF59D",
|
|
||||||
"#FFF176",
|
|
||||||
"#FFEE58",
|
|
||||||
"#FFEB3B",
|
|
||||||
"#FDD835",
|
|
||||||
"#FBC02D",
|
|
||||||
"#F9A825",
|
|
||||||
"#F57F17",
|
|
||||||
"#FFFF8D",
|
|
||||||
"#FFFF00",
|
|
||||||
"#FFEA00",
|
|
||||||
"#FFD600"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Amber",
|
|
||||||
"colors": [
|
|
||||||
"#FFF8E1",
|
|
||||||
"#FFECB3",
|
|
||||||
"#FFE082",
|
|
||||||
"#FFD54F",
|
|
||||||
"#FFCA28",
|
|
||||||
"#FFC107",
|
|
||||||
"#FFB300",
|
|
||||||
"#FFA000",
|
|
||||||
"#FF8F00",
|
|
||||||
"#FF6F00",
|
|
||||||
"#FFE57F",
|
|
||||||
"#FFD740",
|
|
||||||
"#FFC400",
|
|
||||||
"#FFAB00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Orange",
|
|
||||||
"colors": [
|
|
||||||
"#FFF3E0",
|
|
||||||
"#FFE0B2",
|
|
||||||
"#FFCC80",
|
|
||||||
"#FFB74D",
|
|
||||||
"#FFA726",
|
|
||||||
"#FF9800",
|
|
||||||
"#FB8C00",
|
|
||||||
"#F57C00",
|
|
||||||
"#EF6C00",
|
|
||||||
"#E65100",
|
|
||||||
"#FFD180",
|
|
||||||
"#FFAB40",
|
|
||||||
"#FF9100",
|
|
||||||
"#FF6D00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Deep Orange",
|
|
||||||
"colors": [
|
|
||||||
"#FBE9E7",
|
|
||||||
"#FFCCBC",
|
|
||||||
"#FFAB91",
|
|
||||||
"#FF8A65",
|
|
||||||
"#FF7043",
|
|
||||||
"#FF5722",
|
|
||||||
"#F4511E",
|
|
||||||
"#E64A19",
|
|
||||||
"#D84315",
|
|
||||||
"#BF360C",
|
|
||||||
"#FF9E80",
|
|
||||||
"#FF6E40",
|
|
||||||
"#FF3D00",
|
|
||||||
"#DD2C00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Brown",
|
|
||||||
"colors": [
|
|
||||||
"#EFEBE9",
|
|
||||||
"#D7CCC8",
|
|
||||||
"#BCAAA4",
|
|
||||||
"#A1887F",
|
|
||||||
"#8D6E63",
|
|
||||||
"#795548",
|
|
||||||
"#6D4C41",
|
|
||||||
"#5D4037",
|
|
||||||
"#4E342E",
|
|
||||||
"#3E2723"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Gray",
|
|
||||||
"colors": [
|
|
||||||
"#FAFAFA",
|
|
||||||
"#F5F5F5",
|
|
||||||
"#EEEEEE",
|
|
||||||
"#E0E0E0",
|
|
||||||
"#BDBDBD",
|
|
||||||
"#9E9E9E",
|
|
||||||
"#757575",
|
|
||||||
"#616161",
|
|
||||||
"#424242",
|
|
||||||
"#212121"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shade": "Blue Gray",
|
|
||||||
"colors": [
|
|
||||||
"#ECEFF1",
|
|
||||||
"#CFD8DC",
|
|
||||||
"#B0BEC5",
|
|
||||||
"#90A4AE",
|
|
||||||
"#78909C",
|
|
||||||
"#607D8B",
|
|
||||||
"#546E7A",
|
|
||||||
"#455A64",
|
|
||||||
"#37474F",
|
|
||||||
"#263238"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in a new issue