Compare commits

..

103 commits

Author SHA1 Message Date
c3e02a0d1a feat: moving from old name conventions. 2024-12-25 00:43:55 +05:00
Magnus Hovland Hoff
b034dcf61a Update bart dependency, which depended on a yanked crate version. 2023-06-21 21:59:17 +02:00
Magnus Hovland Hoff
03227d6aa9 Remove some redundant "extern crate" statements 2022-04-03 22:21:04 +02:00
Magnus Hovland Hoff
0521f0ce5a Upgrade quote 2022-04-03 22:16:45 +02:00
Magnus Hovland Hoff
25dcb85c24 Warning seems to have disappeared with updated diesel dependency 2022-04-03 22:10:29 +02:00
Magnus Hovland Hoff
bf34e2923b Addressed many cargo clippy complaints 2022-04-03 22:09:36 +02:00
Magnus Hovland Hoff
95a73b9471 cargo clippy --fix 2022-04-03 14:29:35 +02:00
Magnus Hovland Hoff
9320d14d89 Manually fix remaining compile warnings. 2022-04-03 14:19:56 +02:00
Magnus Hovland Hoff
e0f52cd031 Upgrade diesel dependencies.
When switching to edition=2021, the build fails with a linking error to -lsqlite3.
Upgrading libsqlite3-sys unfortunately does not fix this.
2022-04-03 14:01:45 +02:00
Magnus Hovland Hoff
203a701517 Upgrade lazy_static to get rid of compile warning 2022-04-03 13:49:15 +02:00
Magnus Hovland Hoff
9f80ced3ec cargo fmt 2022-04-03 13:47:43 +02:00
Magnus Hovland Hoff
26fe2b64da Update edition, cargo fix and cargo fix --edition 2022-04-03 13:45:50 +02:00
Magnus Hovland Hoff
3f71040aec Disable auto-deploy.
I would like to keep continuous builds available, but the auto-deploy has triggered
GitHub releases for every build, which is very confusing documentation. Also, Travis
and other CI services, do not seem to want to offer a free tier any longer. Considering
switching to running continuous build on my own hardware somehow.
2022-04-03 13:35:10 +02:00
Magnus Hovland Hoff
f675896054 Update API key for Github deploy 2021-05-23 21:17:42 +02:00
Magnus Hovland Hoff
3b16598444 cargo update
Fix broken build
2021-05-01 20:18:41 +02:00
Magnus Hovland Hoff
fef213f9f3 cargo update
This fixes build breakage caused by rotten dependency
2020-05-25 21:37:24 +02:00
Magnus Hovland Hoff
f96cc8dac5 Silence warnings caused by Diesel on Rust >=1.29, also in build.rs 2018-10-27 15:30:33 +02:00
Magnus Hovland Hoff
a20117a42c Silence warnings caused by Diesel on Rust >=1.29.
I expect this to be fixed in a future release of Diesel
2018-10-27 11:42:09 +02:00
Magnus Hovland Hoff
58a859b014 Completely disable setting focus.
setSelectionRange causes Safari to set focus to the element. Trying to get rid of distracting focus changes that pops up an on-screen keyboard
2018-10-08 07:53:41 +02:00
Magnus Hovland Hoff
318273a75d Improve placeholder styling.
Apply placeholder styling to title editor as well
2018-10-08 07:49:14 +02:00
Magnus Hovland Hoff
0a64a274ac Bugfix. Anonymous edits would have a stray period. 2018-10-07 13:54:09 +02:00
Magnus Hovland Hoff
ccbabb86f8 Make theme optional when updating articles.
This makes sense from an API design perspective
2018-10-05 13:07:19 +02:00
Magnus Hovland Hoff
09c68c5993 Select random theme for new articles server side.
This supports user agents with disabled javascript (noscript)
2018-10-05 11:56:53 +02:00
Magnus Hovland Hoff
7373af0417 Select random theme for new articles client side 2018-10-05 11:19:47 +02:00
Magnus Hovland Hoff
65ad262bd8 Bugfix.
Cancel button is always disabled and shown conditionally
2018-10-05 11:03:41 +02:00
Magnus Hovland Hoff
cffcc93b15 Add tests for automatically slugged links 2018-10-03 22:35:47 +02:00
Magnus Hovland Hoff
575c18f915 Support automatically slugged links 2018-10-03 18:24:55 +02:00
Magnus Hovland Hoff
6d1c9967aa Correctly mark unused variable 2018-10-03 18:12:27 +02:00
Magnus Hovland Hoff
748459483e Show author for diffs that show only one changeset 2018-10-02 08:44:06 +02:00
Magnus Hovland Hoff
01ceda8015 Use correct number for generating pagination link 2018-10-01 22:58:40 +02:00
Magnus Hoff
16384c9f83 Inconsequential change to trigger CI pipeline 2018-10-01 10:03:24 +02:00
Magnus Hovland Hoff
d5410f2a22 Revert focusing the body text editor when entering edit mode.
This behaves badly in multiple mobile browsers, causing confusion. Perhaps it is better to disable this feature altogether
2018-10-01 09:02:55 +02:00
Magnus Hovland Hoff
17c23da9bf Better scroll handling when toggling editing.
It will now retain the scroll position relative to the full document height when entering and leaving editing mode. This places the viewport closer to the contents you were watching. This is likely less disorienting
2018-10-01 09:01:27 +02:00
Magnus Hovland Hoff
619ba14b3f Fix editor controls layout in narrow layout 2018-10-01 08:36:44 +02:00
Magnus Hoff
58283a601c Also disable form elements via CSS pointer-events when not in edit mode.
This lets us visually transition the controls without accidentally enabling interaction
2018-09-30 22:33:40 +02:00
Magnus Hoff
c6dd37ed9e Centralize handling of form elements state handling.
This makes it easier to maintain state changes. Seems to handle all states well now
2018-09-30 22:30:52 +02:00
Magnus Hoff
85014d2789 Include theme in response when creating new article.
Bug caused by duplication of code.
2018-09-30 22:29:37 +02:00
Magnus Hovland Hoff
a81a568ee2 Forward the edit-state when redirecting to renamed articles.
This improves usability with noscript
2018-09-24 23:01:01 +02:00
Magnus Hovland Hoff
e92c9695be Fix editing of new articles with noscript.
This also generalizes the code. Neat!
2018-09-24 22:55:10 +02:00
Magnus Hovland Hoff
0439ca0d8e Avoid storing build cache in travis,
it costs more than we gain
2018-09-24 18:30:17 +02:00
Magnus Hovland Hoff
62378007b1 Reset theme when editing is canceled 2018-09-24 18:21:50 +02:00
Magnus Hoff
3bbe5840ee Implement theme picker UI 2018-09-24 08:43:36 +02:00
Magnus Hovland Hoff
baaab6ebc8 Store theme explicitly in database. Propagate theme both ways between db and frontend 2018-09-23 22:38:18 +02:00
Magnus Hovland Hoff
fe0011e757 Allow build.rs to figure out the correct database schema 2018-09-23 21:39:54 +02:00
Magnus Hovland Hoff
828490df3b Update serde_urlencoded dependency for bugfix 2018-09-23 21:39:09 +02:00
Magnus Hovland Hoff
c1fcc80cf0 Minor fix for making Theme insertable with Diesel 2018-09-23 21:38:17 +02:00
Magnus Hovland Hoff
8f1e95bdde Add theme to ArticleRevisionStub, RebaseResult and related.
This paves the way for explicitly storing the theme in the database
2018-09-22 23:16:58 +02:00
Magnus Hovland Hoff
c82228f019 Propagate theme in ArticleRevisions from the state struct 2018-09-21 08:57:35 +02:00
Magnus Hovland Hoff
b777a92a48 Expose theme_from_str_hash to SQL.
To be used in a db migration for storing the previously implicit value
2018-09-20 23:17:25 +02:00
Magnus Hovland Hoff
ca1e072d9b Add more tests for using Theme with the database 2018-09-20 08:37:27 +02:00
Magnus Hovland Hoff
df066c611d Add support for using Theme with the database 2018-09-19 22:56:12 +02:00
Magnus Hovland Hoff
f961699f0f Rename function to avoid confusion 2018-09-19 08:20:43 +02:00
Magnus Hovland Hoff
6118f14bb0 Explicitly set theme for layout.
Refactoring in anticipation of letting the user choose theme
2018-09-18 23:11:25 +02:00
Magnus Hovland Hoff
c1dcb1de64 Make responsibility for converting a theme to a css class to the theme module 2018-09-18 19:43:33 +02:00
Magnus Hovland Hoff
d4e8277f2a Dependency for previous commit 2018-09-18 19:21:36 +02:00
Magnus Hovland Hoff
a65e85f242 Refactor Theme handling to a new module.
Fundamentals for communicating about themes with the database and over http
2018-09-18 07:56:58 +02:00
Magnus Hovland Hoff
ecf4c1e98e Disable save hotkey when it is not appropriate 2018-09-16 22:31:57 +02:00
Magnus Hovland Hoff
534dffdfe3 Show disabled cancel button instead of removing it.
This fixes two problems caused by the removal of the button: Broken layout and broken JS
2018-09-16 22:25:28 +02:00
Magnus Hovland Hoff
999253a778 Place text caret at end of text to reduce visual distraction caused by stuff moving around 2018-09-16 12:16:03 +02:00
Magnus Hovland Hoff
a00cdf6394 Position editor controls with units that are affected by the scroll bar 2018-09-16 12:10:02 +02:00
Magnus Hovland Hoff
8d86e8937a Capitalize placeholder in search input.
I think I like this better
2018-09-16 12:06:20 +02:00
Magnus Hovland Hoff
830f641167 Fine-tune margin 2018-09-13 08:48:09 +02:00
Magnus Hoff
d6e1015197 Reset style for Safari to make transition between view and edit more seamless 2018-09-11 19:53:14 +02:00
Magnus Hoff
9c67333b87 Wider top margin for better visual balance 2018-09-11 19:19:22 +02:00
Magnus Hovland Hoff
7b1a0256e1 Add rainbow bar to test-themes 2018-08-31 21:30:46 +02:00
Magnus Hovland Hoff
94db59c44c Add hotkey for saving 2018-08-22 08:14:25 +02:00
Magnus Hovland Hoff
42e7857fcd Revert "Attempt to improve overscroll in Apple browsers"
This reverts commit 0847cb5c4d.
2018-08-21 21:57:40 +02:00
Magnus Hovland Hoff
0847cb5c4d Attempt to improve overscroll in Apple browsers 2018-08-21 19:06:53 +02:00
Magnus Hovland Hoff
b8da0ff753 Fix breakpoint size for editor controls 2018-08-21 19:02:38 +02:00
Magnus Hovland Hoff
096da6ef38 Iterate on editor controls design 2018-08-21 18:40:44 +02:00
Magnus Hoff
c94bf91fc2 Iterate on editor controls styling 2018-08-20 23:25:03 +02:00
Magnus Hoff
b93c79c479 Update styling of .notice 2018-08-20 22:47:52 +02:00
Magnus Hoff
b8a4368219 Improve contrast for placeholder text in search box 2018-08-20 22:32:04 +02:00
Magnus Hoff
8500075357 Update styling of hr 2018-08-20 22:31:35 +02:00
Magnus Hoff
53e983bee9 Iterate on themes
Select yellow alternate colors for some hues where blue works poorly
2018-07-21 11:54:03 +02:00
Magnus Hoff
c18b8f45d1 Iterate on the themes.
Always white text
2018-07-20 23:35:33 +02:00
Magnus Hoff
d3a50b0bc0 Iterate on color schemes.
Improve descision of proper contrast
2018-07-19 08:53:05 +02:00
Magnus Hoff
d905c1aa62 Unify appearence of placeholder text across browsers 2018-07-19 08:43:22 +02:00
Magnus Hoff
5a2be1d0a8 Update print style.
Hide outgoing link-indicator
2018-07-19 08:39:14 +02:00
Magnus Hoff
c2c0bae335 Avoid changing layout on hover 2018-07-19 08:36:47 +02:00
Magnus Hoff
e26e60ce2c Iterate on the design
Less box-shadow
2018-07-16 22:11:53 +02:00
Magnus Hoff
d5bb94dfb6 Iterate on the color palettes
Darker link color for light backgrounds
2018-07-16 20:10:30 +02:00
Magnus Hoff
05b12501a3 Iterate on the color palettes 2018-07-16 08:30:29 +02:00
Magnus Hoff
c201bb4bc4 Add test page for themes 2018-07-10 08:46:51 +02:00
Magnus Hoff
963d70ff7a Add dynamic-assets feature to facilitate rapid feedback when working on the assets 2018-07-09 21:27:34 +02:00
Magnus Hoff
8b0e58c24c Update about text 2018-07-08 22:34:04 +02:00
Magnus Hoff
862632335b Minor
This CSS is now included directly from the HTML. The real fix, however, is to preprocess the CSS into one file
2018-06-25 08:05:42 +02:00
Magnus Hoff
c0ce03973a Update print style for new theme 2018-06-25 08:03:40 +02:00
Magnus Hoff
0b5bff6356 Refactor handling of generated unique names for resources 2018-06-24 23:00:35 +02:00
cmal
77210a9692 Do not underline external link marker on hover (#69) 2018-06-24 15:04:37 +02:00
Magnus Hoff
e4fa7ed89a Credit cmal as new contributor 2018-06-24 14:56:01 +02:00
cmal
a85abf1ccb Added marker for external links (#63) 2018-06-23 10:35:22 +02:00
Magnus Hoff
bddf4c0225 CSS tweaks for iOS 2018-06-17 23:02:36 +02:00
Magnus Hoff
f7227bf3d4 Fix misalignment 2018-06-17 22:20:12 +02:00
Magnus Hoff
38c70f7b25 Select persistent theme per page based on title 2018-06-17 21:25:35 +02:00
Magnus Hoff
31ace5d4c2 Apply new style to search 2018-06-17 21:16:44 +02:00
Magnus Hoff
e4629d8edb Refactor rendering of pages to centralize header layout.
Convert all pages to new layout
2018-06-17 10:43:47 +02:00
Magnus Hoff
2b27e27a9b Tweak layout.html.
This fixes #67
2018-06-16 14:51:13 +02:00
Magnus Hoff
0a48ff2a54 Factor choice of theme into Layout struct 2018-06-16 14:30:18 +02:00
Magnus Hoff
7e6fe36ea0 Serve articles with a random theme 2018-06-16 11:24:34 +02:00
Magnus Hoff
4516534b39 Start trying out implementing new layout 2018-06-16 10:58:00 +02:00
Magnus Hoff
67ac61ee42 Add CSS defining color schemes/themes 2018-06-16 10:51:51 +02:00
Magnus Hoff
a40d45b197 Add utility for generating CSS for color themes 2018-06-16 10:32:33 +02:00
64 changed files with 4531 additions and 2270 deletions

View file

@ -19,21 +19,6 @@ script:
- 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
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:
except:
- "/^untagged-/"

1566
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,84 +1,93 @@
[package]
authors = ["Magnus Hoff <maghoff@gmail.com>"]
description = "A wiki engine"
license = "GPL-3.0"
name = "sausagewiki"
version = "0.1.0-dev"
description = "A wiki engine"
authors = ["Magnus Hoff <maghoff@gmail.com>"]
license = "GPL-3.0"
edition = "2018"
[profile.release]
panic = "abort"
[build-dependencies]
quote = "1.0.17"
walkdir = "1"
[dev-dependencies]
indoc = "0.2"
matches = "0.1"
[build-dependencies.diesel]
default-features = false
features = ["sqlite", "chrono"]
version = "1.4.8"
[build-dependencies.diesel_migrations]
default-features = false
features = ["sqlite"]
version = "1.4.0"
[dependencies]
bart = "0.1.4"
bart_derive = "0.1.4"
bart = "0.1.6"
bart_derive = "0.1.6"
chrono = "0.4"
clap = "2.31"
diff = "0.1"
futures = "0.1"
futures-cpupool = "0.1"
hyper = "0.11"
lazy_static = "0.2"
lazy_static = "1.4.0"
maplit = "1"
percent-encoding = "1.0"
r2d2 = "0.8"
r2d2-diesel = "1.0.0"
regex = "0.2"
seahash = "3.0.5"
serde = "1.0.0"
serde_derive = "1.0.0"
serde_json = "1.0"
serde_urlencoded = "0.5"
serde_urlencoded = "0.5.3"
slug = "0.1"
titlecase = "0.10"
tokio-io = "0.1"
tokio-proto = "0.1"
tokio-service = "0.1"
serde_plain = "0.3.0"
rand = "0.5.5"
[dependencies.libsqlite3-sys]
features = ["bundled"]
version = "0.9.1"
[dependencies.codegen]
path = "libs/codegen"
[dependencies.diesel]
default-features = false
features = ["sqlite", "chrono"]
version = "1.3.0"
[dependencies.diesel_migrations]
default-features = false
features = ["sqlite"]
version = "1.3.0"
version = "1.4.8"
[dependencies.diesel_infer_schema]
default-features = false
features = ["sqlite"]
version = "1.3.0"
version = "1.4.0"
[dependencies.diesel_migrations]
default-features = false
features = ["sqlite"]
version = "1.4.0"
[dependencies.libsqlite3-sys]
features = ["bundled"]
version = "<0.23.0"
[dependencies.num]
default-features = false
version = "0.1"
[dependencies.pulldown-cmark]
default-features = false
git = "https://github.com/maghoff/pulldown-cmark.git"
default-features = false
[dependencies.codegen]
path = "libs/codegen"
[dev-dependencies]
indoc = "1.0.4"
matches = "0.1"
[build-dependencies]
quote = "0.3.10"
walkdir = "1"
[features]
dynamic-assets = []
[build-dependencies.diesel]
default-features = false
features = ["sqlite", "chrono"]
version = "1.3.0"
[profile]
[build-dependencies.diesel_migrations]
default-features = false
features = ["sqlite"]
version = "1.3.0"
[profile.release]
panic = "abort"
[workspace]

View file

@ -1,3 +1,5 @@
"use strict";
function autosizeTextarea(textarea, shadow) {
shadow.style.width = textarea.clientWidth + "px";
shadow.value = textarea.value;
@ -6,16 +8,17 @@ function autosizeTextarea(textarea, shadow) {
function queryArgsFromForm(form) {
const items = [];
for (const {name, value} of form.elements) {
for (const {name, value, type, checked} of form.elements) {
if (!name) continue;
if (type === "radio" && !checked) continue;
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
}
return items.join('&');
}
function isEdited(form) {
for (const {name, value, defaultValue} of form.elements) {
if (name && (value !== defaultValue)) return true;
for (const {name, value, defaultValue, checked, defaultChecked} of form.elements) {
if (name && ((value !== defaultValue) || (checked !== defaultChecked))) return true;
}
return false;
}
@ -56,40 +59,77 @@ function confirmDiscard() {
return popup(instantiate("confirm-discard"));
}
let hasBeenOpen = false;
const state = {
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() {
const bodyElement = document.querySelector("body");
const container = document.querySelector(".container");
const rendered = container.querySelector(".rendered");
const editor = container.querySelector(".editor");
const textarea = editor.querySelector('textarea[name="body"]');
const shadow = editor.querySelector('textarea.shadow-control');
const form = editor.querySelector("form");
const cancel = editor.querySelector('.cancel');
const form = document.getElementById('article-editor');
const cancel = form.querySelector('.cancel');
const cancelButton = form.querySelector('button.button-cancel');
const cancelInteractionGroup = form.querySelector(".cancel-interaction-group");
const footer = document.querySelector("footer");
const lastUpdated = footer.querySelector(".last-updated");
textarea.style.height = rendered.clientHeight + "px";
container.classList.add('edit');
retainScrollRatio(() => {
container.classList.add('edit');
autosizeTextarea(textarea, shadow);
});
updateFormEnabledState();
autosizeTextarea(textarea, shadow);
textarea.focus();
if (hasBeenOpen) return;
hasBeenOpen = true;
if (state.hasBeenOpen) return;
state.hasBeenOpen = true;
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
form.addEventListener("submit", function (ev) {
ev.preventDefault();
ev.stopPropagation();
function updateFormEnabledState() {
const baseEnabled = !state.saving && state.editing();
const enabled = {
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);
textarea.disabled = true;
// TODO Disable other interaction as well: title editor, cancel and OK buttons
fetch(
form.getAttribute("action"),
@ -109,7 +149,8 @@ function openEditor() {
if (probablyLoginRedirect) {
return loginDialog(response.url)
.then(() => {
textarea.disabled = false;
state.saving = false;
updateFormEnabledState();
});
}
@ -117,8 +158,10 @@ function openEditor() {
return response.json()
.then(result => {
// Update url-bar, page title and footer
window.history.replaceState(null, result.title, result.slug == "" ? "." : result.slug);
// Update url-bar, page title, footer and cancel link
const url = result.slug == "" ? "." : result.slug;
window.history.replaceState(null, result.title, url);
cancel.setAttribute("href", url);
document.querySelector("title").textContent = result.title;
lastUpdated.innerHTML = result.last_updated;
lastUpdated.classList.remove("missing");
@ -129,17 +172,22 @@ function openEditor() {
form.elements.title.value = result.title;
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:
form.elements.base_revision.value = result.revision;
for (const element of form.elements) {
element.defaultValue = element.value;
element.defaultChecked = element.checked;
}
if (!result.conflict) {
container.classList.remove('edit');
closeEditor();
}
textarea.disabled = false;
state.saving = false;
updateFormEnabledState();
autosizeTextarea(textarea, shadow);
if (result.conflict) {
@ -149,23 +197,37 @@ function openEditor() {
}
});
}).catch(err => {
textarea.disabled = false;
state.saving = false;
updateFormEnabledState();
console.error(err);
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) {
ev.preventDefault();
ev.stopPropagation();
Promise.resolve(!isEdited(form) || confirmDiscard())
.then(doReset => {
if (doReset) {
container.classList.remove('edit');
form.reset();
}
});
doCancel();
});
window.addEventListener("beforeunload", function (ev) {
@ -174,8 +236,43 @@ function openEditor() {
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
.getElementById("openEditor")
.addEventListener("click", function (ev) {

View file

@ -1,15 +1,11 @@
@font-face {
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');
}
.prototype {
display: none;
}
input {
margin: 0; /* reset for Safari */
}
html {
font-family: "Apple Garamond", "Baskerville",
"Times New Roman", "Droid Serif", "Times",
@ -17,8 +13,6 @@ html {
}
h1 {
font-family: 'Amatic SC', sans-serif;
font-weight: normal;
font-style: normal;
@ -64,15 +58,16 @@ h1+*, h2+*, h3+*, h4+*, h5+*, h6+* {
article>hr {
border: none;
border-top: 1px solid black;
max-width: 400px;
width: 70%;
border-top: 6px solid var(--theme-main);
width: 40px;
margin: 20px auto;
}
.notice {
background: lightyellow;
padding: 16px 48px;
background: var(--theme-main);
color: var(--theme-text);
padding: 1px 24px;
font-size: 18px;
line-height: 32px;
@ -81,6 +76,22 @@ article>hr {
width: 100%;
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 {
box-sizing: border-box;
@ -97,7 +108,7 @@ header {
}
article {
margin: 0 auto 120px auto;
margin: 50px auto 120px auto;
font-size: 18px;
line-height: 32px;
@ -148,7 +159,19 @@ pre {
}
a {
color: #457796;
color: #1976D2;
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;
}
@ -182,10 +205,10 @@ body {
footer {
padding: 0 8px;
padding: 16px 8px 16px 8px;
background: #f8f8f8;
color: #444;
background: var(--theme-main);
color: var(--theme-text);
text-align: center;
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
@ -193,6 +216,10 @@ footer {
"Droid Sans", "Helvetica Neue", sans-serif;
}
footer a {
color: var(--theme-link);
}
ul.dense {
list-style: none;
padding: 0;
@ -248,6 +275,11 @@ h1>input {
width: 100%;
}
.hero input {
background: var(--theme-input);
color: var(--theme-text);
}
.shadow-control {
visibility: hidden;
position: fixed;
@ -277,18 +309,163 @@ h1>input {
bottom: 0;
left: 0;
background: #91A238;
padding: 10px 20px;
box-sizing: border-box;
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;
}
@media (min-width: 630px) {
.editor-controls {
position: fixed;
left: auto;
right: 20px;
bottom: 20px;
.edit .editor-controls {
transform: translate(0, 0);
transition-timing-function: cubic-bezier(.17,.84,.44,1);
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
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 {
border-radius: 2px;
position: fixed;
left: calc(50% + 320px);
width: 140px;
top: calc(50% - 55px);
height: 110px;
padding: 10px;
transform: translate(20px, 0);
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;
}
}
@ -313,18 +490,18 @@ article ul.search-results {
display: block;
color: inherit;
text-decoration: none;
border: 1px solid #ccc;
padding: 8px;
padding: 8px 16px;
background: white;
color: black;
}
.search-result a:hover, .search-result a:focus {
background: #0074D9;
border-color: #0074D9;
color: white;
background: var(--theme-main);
color: var(--theme-text);
}
.search {
text-align: center;
margin-top: 30px;
margin-top: 45px;
position: relative;
}
@ -333,16 +510,18 @@ input[type="search"]::-webkit-search-decoration {
}
input[type="search"] {
-webkit-appearance: textfield;
-webkit-appearance: none;
border-radius: 0;
box-sizing: border-box;
width: 100%;
max-width: 300px;
transition: max-width 200ms;
padding: 0 16px;
margin: 0;
border: 1px solid #ccc;
border: none;
background: var(--theme-input);
color: var(--theme-text);
font: inherit;
font-size: 18px;
@ -350,13 +529,12 @@ input[type="search"] {
line-height: 32px;
height: 34px;
border-radius: 18px;
text-overflow: ellipsis;
}
.search input:focus, .search.focus input {
max-width: 300px;
border-color: #999;
input[type="search"]::placeholder, .hero input::placeholder {
color: var(--theme-text);
opacity: 0.6;
}
.search .live-results {
@ -364,9 +542,8 @@ input[type="search"] {
box-sizing: border-box;
width: 100%;
max-width: 266px; /* 300px - padding - border */
max-width: 300px;
background: white;
padding: 0;
margin: 0 auto;
@ -376,6 +553,15 @@ input[type="search"] {
max-height: 0px;
}
.search-widget-container {
border-radius: 2px;
overflow: hidden;
width: 100%;
max-width: 300px;
display: inline-block;
}
.live-results.show {
max-height: 500px;
}
@ -383,13 +569,8 @@ input[type="search"] {
.live-results .search-result {
margin: 0;
}
.live-results a {
border-top: none;
}
.live-results .search-result.error {
border: 1px solid #ccc;
border-top: none;
padding: 8px;
color: #888;
}
@ -397,16 +578,24 @@ input[type="search"] {
@media (min-width: 630px) {
.search {
text-align: right;
height: 38px;
position: relative;
}
.search input {
max-width: 125px;
.search-widget-container {
position: absolute;
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 {
position: absolute;
right: 8px;
margin: 0 16px;
width: 100%;
max-height: 0px;
}
@ -447,7 +636,7 @@ input[type="search"] {
text-align: center;
background: #eee;
box-shadow: 2px 2px 8px rgba(0,0,0, 0.25);
box-shadow: 0px 5px 20px rgba(0,0,0, 0.2);
}
.popup>.message {
@ -499,6 +688,14 @@ input[type="search"] {
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 {
/* This doesn't work at all, but it might start to! */
break-after: avoid;
@ -511,10 +708,23 @@ input[type="search"] {
font-weight: normal !important;
}
a[href^="http"]::after {
display: none;
}
article {
margin: 0 auto;
}
article>hr {
border-color: black;
}
h1 {
font-size: 22pt;
line-height: 33pt;
}
article, h2, h3, h4, h5, h6, .notice {
font-size: 12pt;
line-height: 18pt;

54
assets/test-themes.html Normal file
View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Test themes &ndash; 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>

134
assets/themes.css Normal file
View file

@ -0,0 +1,134 @@
.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;
}

View file

@ -1,10 +1,9 @@
#[macro_use] extern crate quote;
#[macro_use] extern crate diesel;
extern crate diesel_migrations;
extern crate walkdir;
#[macro_use]
extern crate diesel;
use diesel::Connection;
use diesel::prelude::*;
use diesel::Connection;
use quote::quote;
use std::env;
use std::fs::File;
use std::io::prelude::*;
@ -15,52 +14,64 @@ use walkdir::WalkDir;
mod sqlfunc {
use diesel::sql_types::Text;
sql_function!(fn markdown_to_fts(text: Text) -> Text);
sql_function!(fn theme_from_str_hash(text: Text) -> Text);
}
fn main() {
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 = db_path.to_str().expect("Will only work for Unicode-representable paths");
let db_path = db_path
.to_str()
.expect("Will only work for Unicode-representable paths");
let _ignore_failure = std::fs::remove_file(db_path);
let connection = SqliteConnection::establish(db_path)
.expect(&format!("Error esablishing a database connection to {}", db_path));
.unwrap_or_else(|_| panic!("Error esablishing a database connection to {}", db_path));
// 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)
.expect("Should be able to enable foreign keys");
sqlfunc::markdown_to_fts::register_impl(&connection, |_: String| -> String { unreachable!() }).unwrap();
sqlfunc::markdown_to_fts::register_impl(&connection, |_: String| -> String { unreachable!() })
.unwrap();
sqlfunc::theme_from_str_hash::register_impl(&connection, |_: String| -> String {
unreachable!()
})
.unwrap();
diesel_migrations::run_pending_migrations(&connection).unwrap();
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");
file.write_all(quote! {
mod __diesel_infer_schema_articles {
infer_table_from_schema!(#db_path, "articles");
}
pub use self::__diesel_infer_schema_articles::*;
file.write_all(
quote! {
mod __diesel_infer_schema_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");
mod __diesel_infer_schema_article_revisions {
infer_table_from_schema!(#db_path, "article_revisions");
}
pub use self::__diesel_infer_schema_article_revisions::*;
}
pub use self::__diesel_infer_schema_article_revisions::*;
}.as_str().as_bytes()).expect("Unable to write to file");
.to_string()
.as_bytes(),
)
.expect("Unable to write to file");
for entry in WalkDir::new("migrations").into_iter().filter_map(|e| e.ok()) {
for entry in WalkDir::new("migrations")
.into_iter()
.filter_map(|e| e.ok())
{
println!("cargo:rerun-if-changed={}", entry.path().display());
}
// 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);
}
}

View file

@ -5,3 +5,4 @@ This is a list of people who have contributed to this project.
- Magnus Hoff (maghoff@gmail.com)
- Johannes Hoff (johanneshoff@gmail.com)
- Konstantin Yegupov (kyegupov4@gmail.com)
- cmal (paul@cmal.info)

View file

@ -1,11 +1,13 @@
#![recursion_limit="128"]
#![recursion_limit = "128"]
#[macro_use] extern crate quote;
#[macro_use] extern crate serde_derive;
#[macro_use]
extern crate quote;
#[macro_use]
extern crate serde_derive;
extern crate base64;
extern crate proc_macro;
extern crate serde_json;
extern crate serde;
extern crate serde_json;
extern crate sha2;
extern crate syn;

View file

@ -2,13 +2,10 @@ use std::fs::File;
use proc_macro::TokenStream;
use quote;
use serde_json;
use serde::de::IgnoredAny;
use serde_json;
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)]
pub enum LicenseId {
@ -22,7 +19,7 @@ impl LicenseId {
fn include_notice(&self) -> bool {
use self::LicenseId::*;
match self {
&Mpl2 => false,
Mpl2 => false,
_ => true,
}
}
@ -32,10 +29,10 @@ impl quote::ToTokens for LicenseId {
fn to_tokens(&self, tokens: &mut quote::Tokens) {
use self::LicenseId::*;
tokens.append(match self {
&Bsd3Clause => "Bsd3Clause",
&Mit => "Mit",
&Mpl2 => "Mpl2",
&Ofl11 => "Ofl11",
Bsd3Clause => "Bsd3Clause",
Mit => "Mit",
Mpl2 => "Mpl2",
Ofl11 => "Ofl11",
});
}
}
@ -56,12 +53,16 @@ struct LicenseReport {
impl quote::ToTokens for LicenseReport {
fn to_tokens(&self, tokens: &mut quote::Tokens) {
let c: &LicenseDescription = self.conclusion.as_ref().unwrap();
let (name, link, copyright, license) =
(&self.package_name, &c.link, &c.copyright_notice, &c.chosen_license);
let (name, link, copyright, license) = (
&self.package_name,
&c.link,
&c.copyright_notice,
&c.chosen_license,
);
let link = match link {
&Some(ref link) => quote! { Some(#link) },
&None => quote! { None },
let link = match *link {
Some(ref link) => quote! { Some(#link) },
None => quote! { None },
};
let copyright = match license.include_notice() {
@ -85,7 +86,10 @@ pub fn licenses(_input: TokenStream) -> TokenStream {
.iter()
.map(|x| -> Vec<LicenseReport> { serde_json::from_reader(File::open(x).unwrap()).unwrap() })
.map(|x| x.into_iter().filter(|x| x.conclusion.is_ok()))
.fold(vec![], |mut a, b| { a.extend(b); a });
.fold(vec![], |mut a, b| {
a.extend(b);
a
});
license_infos.sort_unstable_by_key(|x| x.package_name.to_lowercase());

View file

@ -10,30 +10,29 @@ fn user_crate_root() -> PathBuf {
std::env::current_dir().expect("Unable to get current directory")
}
fn find_attr<'a>(attrs: &'a Vec<syn::Attribute>, name: &str) -> Option<&'a str> {
attrs.iter()
fn find_attr<'a>(attrs: &'a [syn::Attribute], name: &str) -> Option<&'a str> {
attrs
.iter()
.find(|&x| x.name() == name)
.and_then(|ref attr| match &attr.value {
&syn::MetaItem::NameValue(_, syn::Lit::Str(ref template, _)) => Some(template),
_ => None
.and_then(|attr| match attr.value {
syn::MetaItem::NameValue(_, syn::Lit::Str(ref template, _)) => Some(template),
_ => None,
})
.map(|x| x.as_ref())
}
fn buf_file<P: AsRef<Path>>(filename: P) -> Vec<u8> {
let mut f = File::open(filename)
.expect("Unable to open file for reading");
let mut f = File::open(filename).expect("Unable to open file for reading");
let mut buf = Vec::new();
f.read_to_end(&mut buf)
.expect("Unable to read file");
f.read_to_end(&mut buf).expect("Unable to read file");
buf
}
fn calculate_checksum<P: AsRef<Path>>(filename: P) -> String {
use base64::*;
use sha2::{Sha256, Digest};
use sha2::{Digest, Sha256};
encode_config(&Sha256::digest(&buf_file(filename)), URL_SAFE)
}
@ -42,15 +41,24 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
let s = input.to_string();
let ast = syn::parse_macro_input(&s).unwrap();
let filename = find_attr(&ast.attrs, "filename")
.expect("The `filename` attribute must be specified");
let filename =
find_attr(&ast.attrs, "filename").expect("The `filename` attribute must be specified");
let abs_filename = user_crate_root().join(filename);
let abs_filename = abs_filename.to_str().expect("Absolute file path must be valid Unicode");
let abs_filename = abs_filename
.to_str()
.expect("Absolute file path must be valid Unicode");
let checksum = calculate_checksum(&abs_filename);
let mime = find_attr(&ast.attrs, "mime")
.expect("The `mime` attribute must be specified");
let path: &Path = filename.as_ref();
let resource_name = format!(
"{}-{}.{}",
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 (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
@ -90,12 +98,12 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
}
impl #impl_generics #name #ty_generics #where_clause {
pub fn checksum() -> &'static str {
#checksum
pub fn resource_name() -> &'static str {
#resource_name
}
pub fn etag() -> ::hyper::header::EntityTag {
::hyper::header::EntityTag::new(false, Self::checksum().to_owned())
::hyper::header::EntityTag::new(false, #checksum.to_owned())
}
}
};

View file

@ -0,0 +1,7 @@
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);

View file

@ -72,3 +72,4 @@ Command line arguments
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,
`<address>:<port>`.

View file

@ -1,24 +1,90 @@
use futures::Future;
use web::{Resource, ResponseFuture};
#[cfg(not(feature = "dynamic-assets"))]
mod static_assets {
use crate::web::{Resource, ResponseFuture};
use futures::Future;
use std::collections::HashMap;
#[derive(StaticResource)]
#[filename = "assets/style.css"]
#[mime = "text/css"]
pub struct StyleCss;
// The CSS should be built to a single CSS file at compile time
#[derive(StaticResource)]
#[filename = "assets/themes.css"]
#[mime = "text/css"]
pub struct ThemesCss;
#[derive(StaticResource)]
#[filename = "assets/script.js"]
#[mime = "application/javascript"]
pub struct ScriptJs;
#[derive(StaticResource)]
#[filename = "assets/style.css"]
#[mime = "text/css"]
pub struct StyleCss;
#[derive(StaticResource)]
#[filename = "assets/search.js"]
#[mime = "application/javascript"]
pub struct SearchJs;
#[derive(StaticResource)]
#[filename = "assets/script.js"]
#[mime = "application/javascript"]
pub struct ScriptJs;
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
// 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;
#[derive(StaticResource)]
#[filename = "assets/search.js"]
#[mime = "application/javascript"]
pub struct SearchJs;
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
// 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::*;

View file

@ -7,9 +7,12 @@ pub const PROJECT_NAME: &str = env!("CARGO_PKG_NAME");
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! {
pub static ref VERSION: String = || -> String {
let mut components = Vec::<String>::new();
let mut components = vec![];
#[cfg(debug_assertions)]
components.push("debug".into());
@ -17,7 +20,10 @@ lazy_static! {
#[cfg(test)]
components.push("test".into());
if let None = option_env!("CONTINUOUS_INTEGRATION") {
#[cfg(feature = "dynamic-assets")]
components.push("dynamic-assets".into());
if option_env!("CONTINUOUS_INTEGRATION").is_none() {
components.push("local-build".into());
}
@ -26,26 +32,22 @@ lazy_static! {
}
if let Some(commit) = option_env!("TRAVIS_COMMIT") {
components.push(format!("commit:{}",
components.push(format!(
"commit:{}",
commit
.as_bytes()
.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<_>>()
.join(SOFT_HYPHEN)
));
}
if components.len() > 0 {
if !components.is_empty() {
format!("{} ({})", env!("CARGO_PKG_VERSION"), components.join(" "))
} else {
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());
}

View file

@ -1,10 +1,11 @@
use diesel::prelude::*;
use diesel::expression::sql_literal::sql;
use diesel::prelude::*;
use diesel::sql_types::*;
use r2d2::{CustomizeConnection, Pool};
use r2d2_diesel::{self, ConnectionManager};
use rendering;
use crate::rendering;
use crate::theme;
embed_migrations!();
@ -12,27 +13,35 @@ embed_migrations!();
struct SqliteInitializer;
#[allow(dead_code)]
mod sqlfunc {
pub mod sqlfunc {
use diesel::sql_types::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 {
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)
.map_err(|x| r2d2_diesel::Error::QueryError(x))?;
.map_err(r2d2_diesel::Error::QueryError)?;
sqlfunc::markdown_to_fts::register_impl(
conn,
|text: String| rendering::render_markdown_for_fts(&text)
).map_err(|x| r2d2_diesel::Error::QueryError(x))?;
sqlfunc::markdown_to_fts::register_impl(conn, |text: String| {
rendering::render_markdown_for_fts(&text)
})
.map_err(r2d2_diesel::Error::QueryError)?;
sqlfunc::theme_from_str_hash::register_impl(conn, |title: String| {
theme::theme_from_str_hash(&title)
})
.map_err(r2d2_diesel::Error::QueryError)?;
Ok(())
}
}
pub fn create_pool<S: Into<String>>(connection_string: S) -> Result<Pool<ConnectionManager<SqliteConnection>>, Box<::std::error::Error>> {
pub fn create_pool<S: Into<String>>(
connection_string: S,
) -> Result<Pool<ConnectionManager<SqliteConnection>>, Box<dyn (::std::error::Error)>> {
let manager = ConnectionManager::<SqliteConnection>::new(connection_string);
let pool = Pool::builder()
.connection_customizer(Box::new(SqliteInitializer {}))
@ -53,3 +62,48 @@ pub fn test_connection() -> SqliteConnection {
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);
}
}

View file

@ -1,31 +1,33 @@
#![recursion_limit="128"] // for diesel's infer_schema!
#![allow(clippy::into_iter_on_ref)]
#![allow(clippy::vec_init_then_push)]
#![recursion_limit = "128"]
// for diesel's infer_schema!
#[cfg(test)] #[macro_use] extern crate matches;
#[cfg(test)] #[macro_use] extern crate indoc;
#[macro_use] extern crate bart_derive;
#[macro_use] extern crate codegen;
#[macro_use] #[allow(deprecated)] extern crate diesel_infer_schema;
#[macro_use] extern crate diesel_migrations;
#[macro_use] extern crate diesel;
#[macro_use] extern crate hyper;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate maplit;
#[macro_use] extern crate serde_derive;
extern crate chrono;
extern crate diff;
extern crate futures_cpupool;
extern crate futures;
extern crate percent_encoding;
extern crate pulldown_cmark;
extern crate r2d2_diesel;
extern crate r2d2;
extern crate serde_json;
extern crate serde_urlencoded;
extern crate serde;
extern crate slug;
extern crate titlecase;
#[cfg(test)]
#[macro_use]
extern crate matches;
#[macro_use]
extern crate bart_derive;
#[macro_use]
extern crate codegen;
#[macro_use]
#[allow(clippy::useless_attribute)]
#[allow(deprecated)]
extern crate diesel_infer_schema;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate hyper;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_plain;
use std::net::{IpAddr, SocketAddr};
@ -40,22 +42,26 @@ mod resources;
mod schema;
mod site;
mod state;
mod theme;
mod web;
mod wiki_lookup;
pub fn main(db_file: String, bind_host: IpAddr, bind_port: u16, trust_identity: bool) -> Result<(), Box<std::error::Error>> {
pub fn main(
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 cpu_pool = futures_cpupool::CpuPool::new_num_cpus();
let state = state::State::new(db_pool, cpu_pool);
let lookup = wiki_lookup::WikiLookup::new(state, trust_identity);
let server =
hyper::server::Http::new()
.bind(
&SocketAddr::new(bind_host, bind_port),
move || Ok(site::Site::new(lookup.clone(), trust_identity))
)?;
let server = hyper::server::Http::new()
.bind(&SocketAddr::new(bind_host, bind_port), move || {
Ok(site::Site::new(lookup.clone(), trust_identity))
})?;
println!("Listening on http://{}", server.local_addr().unwrap());

View file

@ -1,11 +1,10 @@
#[macro_use] extern crate lazy_static;
extern crate clap;
extern crate sausagewiki;
#[macro_use]
extern crate lazy_static;
use std::net::IpAddr;
mod build_config;
use build_config::*;
use crate::build_config::*;
const DATABASE: &str = "DATABASE";
const TRUST_IDENTITY: &str = "trust-identity";
@ -18,52 +17,61 @@ fn args<'a>() -> clap::ArgMatches<'a> {
App::new(PROJECT_NAME)
.version(VERSION.as_str())
.about(env!("CARGO_PKG_DESCRIPTION"))
.arg(Arg::with_name(DATABASE)
.help("Sets the database file to use")
.required(true))
.arg(Arg::with_name(PORT)
.help("Sets the listening port")
.short("p")
.long(PORT)
.default_value("8080")
.validator(|x| match x.parse::<u16>() {
Ok(_) => Ok(()),
Err(_) => Err("Must be an integer in the range [0, 65535]".into())
})
.takes_value(true))
.arg(Arg::with_name(ADDRESS)
.help("Sets the IP address to bind to")
.short("a")
.long(ADDRESS)
.default_value("127.0.0.1")
.validator(|x| match x.parse::<IpAddr>() {
Ok(_) => Ok(()),
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 \
.arg(
Arg::with_name(DATABASE)
.help("Sets the database file to use")
.required(true),
)
.arg(
Arg::with_name(PORT)
.help("Sets the listening port")
.short("p")
.long(PORT)
.default_value("8080")
.validator(|x| match x.parse::<u16>() {
Ok(_) => Ok(()),
Err(_) => Err("Must be an integer in the range [0, 65535]".into()),
})
.takes_value(true),
)
.arg(
Arg::with_name(ADDRESS)
.help("Sets the IP address to bind to")
.short("a")
.long(ADDRESS)
.default_value("127.0.0.1")
.validator(|x| match x.parse::<IpAddr>() {
Ok(_) => Ok(()),
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 \
runs behind a reverse proxy which sets this header.")
.long(TRUST_IDENTITY))
runs behind a reverse proxy which sets this header.",
)
.long(TRUST_IDENTITY),
)
.get_matches()
}
fn main() -> Result<(), Box<std::error::Error>> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = args();
const CLAP: &str = "Guaranteed by clap";
const VALIDATOR: &str = "Guaranteed by clap validator";
let db_file = args.value_of(DATABASE).expect(CLAP).to_owned();
let bind_host = args.value_of(ADDRESS).expect(CLAP).parse().expect(VALIDATOR);
let bind_host = args
.value_of(ADDRESS)
.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);
sausagewiki::main(
db_file,
bind_host,
bind_port,
trust_identity,
)
sausagewiki::main(db_file, bind_host, bind_port, trust_identity)
}

View file

@ -1,8 +1,7 @@
use std::fmt::Debug;
use diff;
#[derive(Debug, PartialEq)]
pub struct Chunk<'a, Item: 'a + Debug + PartialEq + Copy>(
pub &'a [diff::Result<Item>],
pub &'a [diff::Result<Item>]
pub &'a [diff::Result<Item>],
);

View file

@ -1,13 +1,12 @@
use std::fmt::Debug;
use diff;
use diff::Result::*;
use super::chunk::Chunk;
pub struct ChunkIterator<'a, Item>
where
Item: 'a + Debug + PartialEq
Item: 'a + Debug + PartialEq,
{
left: &'a [diff::Result<Item>],
right: &'a [diff::Result<Item>],
@ -15,16 +14,19 @@ where
impl<'a, Item> ChunkIterator<'a, Item>
where
Item: 'a + Debug + PartialEq + Eq
Item: 'a + Debug + PartialEq + Eq,
{
pub fn new(left: &'a [diff::Result<Item>], right: &'a [diff::Result<Item>]) -> ChunkIterator<'a, Item> {
pub fn new(
left: &'a [diff::Result<Item>],
right: &'a [diff::Result<Item>],
) -> ChunkIterator<'a, Item> {
ChunkIterator { left, right }
}
}
impl<'a, Item> Iterator for ChunkIterator<'a, Item>
where
Item: 'a + Debug + PartialEq + Copy
Item: 'a + Debug + PartialEq + Copy,
{
type Item = Chunk<'a, Item>;
@ -46,18 +48,18 @@ where
match (self.left.get(li), self.right.get(ri)) {
(Some(&Right(_)), _) => {
li += 1;
},
}
(_, Some(&Right(_))) => {
ri += 1;
},
}
(Some(&Left(_)), Some(_)) => {
li += 1;
ri += 1;
},
}
(Some(_), Some(&Left(_))) => {
li += 1;
ri += 1;
},
}
(Some(&Both(..)), Some(&Both(..))) => {
let chunk = Chunk(&self.left[..li], &self.right[..ri]);
self.left = &self.left[li..];
@ -65,7 +67,7 @@ where
return Some(chunk);
}
_ => {
if self.left.len() > 0 || self.right.len() > 0 {
if !self.left.is_empty() || !self.right.is_empty() {
let chunk = Chunk(self.left, self.right);
self.left = &self.left[self.left.len()..];
self.right = &self.right[self.right.len()..];
@ -81,7 +83,6 @@ where
#[cfg(test)]
mod test {
use super::*;
use diff;
#[test]
fn simple_case() {
@ -94,13 +95,16 @@ mod test {
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
assert_eq!(vec![
Chunk(&oa[0.. 3], &ob[0.. 3]),
Chunk(&oa[3.. 6], &ob[3.. 3]),
Chunk(&oa[6.. 9], &ob[3.. 6]),
Chunk(&oa[9.. 9], &ob[6.. 9]),
Chunk(&oa[9..12], &ob[9..12]),
], chunks);
assert_eq!(
vec![
Chunk(&oa[0..3], &ob[0..3]),
Chunk(&oa[3..6], &ob[3..3]),
Chunk(&oa[6..9], &ob[3..6]),
Chunk(&oa[9..9], &ob[6..9]),
Chunk(&oa[9..12], &ob[9..12]),
],
chunks
);
}
#[test]
@ -113,11 +117,14 @@ mod test {
let ob = diff::chars(o, b);
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
assert_eq!(vec![
Chunk(&oa[0.. 3], &ob[0.. 3]),
Chunk(&oa[3.. 9], &ob[3.. 9]),
Chunk(&oa[9..12], &ob[9..12]),
], chunks);
assert_eq!(
vec![
Chunk(&oa[0..3], &ob[0..3]),
Chunk(&oa[3..9], &ob[3..9]),
Chunk(&oa[9..12], &ob[9..12]),
],
chunks
);
}
#[test]
@ -130,10 +137,10 @@ mod test {
let ob = diff::chars(o, b);
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
assert_eq!(vec![
Chunk(&oa[0..9], &ob[0.. 9]),
Chunk(&oa[9..9], &ob[9..12]),
], chunks);
assert_eq!(
vec![Chunk(&oa[0..9], &ob[0..9]), Chunk(&oa[9..9], &ob[9..12]),],
chunks
);
}
#[test]
@ -146,10 +153,10 @@ mod test {
let ob = diff::chars(o, b);
let chunks = ChunkIterator::new(&oa, &ob).collect::<Vec<_>>();
assert_eq!(vec![
Chunk(&oa[0..6], &ob[0.. 6]),
Chunk(&oa[6..9], &ob[6..12]),
], chunks);
assert_eq!(
vec![Chunk(&oa[0..6], &ob[0..6]), Chunk(&oa[6..9], &ob[6..12]),],
chunks
);
}
#[test]
@ -162,8 +169,6 @@ mod test {
let ob = diff::chars(o, b);
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);
}
}

View file

@ -1,14 +1,12 @@
mod chunk_iterator;
mod chunk;
mod chunk_iterator;
mod output;
use std::fmt::Debug;
use diff;
use self::chunk_iterator::ChunkIterator;
use self::output::*;
use self::output::Output::Resolved;
use self::output::*;
pub use self::output::Output;
@ -19,12 +17,12 @@ pub enum MergeResult<Item: Debug + PartialEq> {
}
impl<'a> MergeResult<&'a str> {
pub fn to_strings(self) -> MergeResult<String> {
pub fn into_strings(self) -> MergeResult<String> {
match self {
MergeResult::Clean(x) => MergeResult::Clean(x),
MergeResult::Conflicted(x) => MergeResult::Conflicted(
x.into_iter().map(Output::to_strings).collect()
)
MergeResult::Conflicted(x) => {
MergeResult::Conflicted(x.into_iter().map(Output::into_strings).collect())
}
}
}
}
@ -33,23 +31,21 @@ impl MergeResult<String> {
pub fn flatten(self) -> String {
match self {
MergeResult::Clean(x) => x,
MergeResult::Conflicted(x) => {
x.into_iter()
.flat_map(|out| match out {
Output::Conflict(a, _o, b) => {
let mut x: Vec<String> = vec![];
x.push("<<<<<<< Your changes:\n".into());
x.extend(a.into_iter().map(|x| format!("{}\n", x)));
x.push("======= Their changes:\n".into());
x.extend(b.into_iter().map(|x| format!("{}\n", x)));
x.push(">>>>>>> Conflict ends here\n".into());
x
},
Output::Resolved(x) =>
x.into_iter().map(|x| format!("{}\n", x)).collect(),
})
.collect()
}
MergeResult::Conflicted(x) => x
.into_iter()
.flat_map(|out| match out {
Output::Conflict(a, _o, b) => {
let mut x: Vec<String> = vec![];
x.push("<<<<<<< Your changes:\n".into());
x.extend(a.into_iter().map(|x| format!("{}\n", x)));
x.push("======= Their changes:\n".into());
x.extend(b.into_iter().map(|x| format!("{}\n", x)));
x.push(">>>>>>> Conflict ends here\n".into());
x
}
Output::Resolved(x) => x.into_iter().map(|x| format!("{}\n", x)).collect(),
})
.collect(),
}
}
}
@ -58,22 +54,21 @@ impl MergeResult<char> {
pub fn flatten(self) -> String {
match self {
MergeResult::Clean(x) => x,
MergeResult::Conflicted(x) => {
x.into_iter()
.flat_map(|out| match out {
Output::Conflict(a, _o, b) => {
let mut x: Vec<char> = vec![];
x.push('<');
x.extend(a);
x.push('|');
x.extend(b);
x.push('>');
x
},
Output::Resolved(x) => x,
})
.collect()
}
MergeResult::Conflicted(x) => x
.into_iter()
.flat_map(|out| match out {
Output::Conflict(a, _o, b) => {
let mut x: Vec<char> = vec![];
x.push('<');
x.extend(a);
x.push('|');
x.extend(b);
x.push('>');
x
}
Output::Resolved(x) => x,
})
.collect(),
}
}
}
@ -85,7 +80,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 hunks: Vec<_> = chunks.map(resolve).collect();
let clean = hunks.iter().all(|x| match x { &Resolved(..) => true, _ => false });
let clean = hunks.iter().all(|x| matches!(x, Resolved(..)));
if clean {
MergeResult::Clean(
@ -93,10 +88,10 @@ pub fn merge_lines<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<&'a st
.into_iter()
.flat_map(|x| match x {
Resolved(y) => y.into_iter(),
_ => unreachable!()
_ => unreachable!(),
})
.collect::<Vec<_>>()
.join("\n")
.join("\n"),
)
} else {
MergeResult::Conflicted(hunks)
@ -110,7 +105,7 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
let chunks = ChunkIterator::new(&oa, &ob);
let hunks: Vec<_> = chunks.map(resolve).collect();
let clean = hunks.iter().all(|x| match x { &Resolved(..) => true, _ => false });
let clean = hunks.iter().all(|x| matches!(x, Resolved(..)));
if clean {
MergeResult::Clean(
@ -118,9 +113,9 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
.into_iter()
.flat_map(|x| match x {
Resolved(y) => y.into_iter(),
_ => unreachable!()
_ => unreachable!(),
})
.collect()
.collect(),
)
} else {
MergeResult::Conflicted(hunks)
@ -129,11 +124,11 @@ pub fn merge_chars<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<char>
#[cfg(test)]
mod test {
use diff;
use indoc::indoc;
use super::*;
use super::output::*;
use super::output::Output::*;
use super::output::*;
use super::*;
#[test]
fn simple_case() {
@ -145,106 +140,141 @@ mod test {
chunks.map(resolve).collect()
}
assert_eq!(vec![
Resolved("aaa".chars().collect()),
Resolved("xxx".chars().collect()),
Resolved("bbb".chars().collect()),
Resolved("yyy".chars().collect()),
Resolved("ccc".chars().collect()),
], merge_chars(
"aaaxxxbbbccc",
"aaabbbccc",
"aaabbbyyyccc",
));
assert_eq!(
vec![
Resolved("aaa".chars().collect()),
Resolved("xxx".chars().collect()),
Resolved("bbb".chars().collect()),
Resolved("yyy".chars().collect()),
Resolved("ccc".chars().collect()),
],
merge_chars("aaaxxxbbbccc", "aaabbbccc", "aaabbbyyyccc",)
);
}
#[test]
fn clean_case() {
assert_eq!(MergeResult::Clean(indoc!("
assert_eq!(
MergeResult::Clean(
indoc!(
"
aaa
xxx
bbb
yyy
ccc
").into()), merge_lines(
indoc!("
"
)
.into()
),
merge_lines(
indoc!(
"
aaa
xxx
bbb
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
bbb
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
bbb
yyy
ccc
"),
));
"
),
)
);
}
#[test]
fn clean_case_chars() {
assert_eq!(MergeResult::Clean("Title".into()), merge_chars(
"Titlle",
"titlle",
"title",
));
assert_eq!(
MergeResult::Clean("Title".into()),
merge_chars("Titlle", "titlle", "title",)
);
}
#[test]
fn false_conflict() {
assert_eq!(MergeResult::Clean(indoc!("
assert_eq!(
MergeResult::Clean(
indoc!(
"
aaa
xxx
ccc
").into()), merge_lines(
indoc!("
"
)
.into()
),
merge_lines(
indoc!(
"
aaa
xxx
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
bbb
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
xxx
ccc
"),
));
"
),
)
);
}
#[test]
fn true_conflict() {
assert_eq!(MergeResult::Conflicted(vec![
Resolved(vec!["aaa"]),
Conflict(vec!["xxx"], vec![], vec!["yyy"]),
Resolved(vec!["bbb", "ccc", ""]),
]), merge_lines(
indoc!("
assert_eq!(
MergeResult::Conflicted(vec![
Resolved(vec!["aaa"]),
Conflict(vec!["xxx"], vec![], vec!["yyy"]),
Resolved(vec!["bbb", "ccc", ""]),
]),
merge_lines(
indoc!(
"
aaa
xxx
bbb
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
bbb
ccc
"),
indoc!("
"
),
indoc!(
"
aaa
yyy
bbb
ccc
"),
));
"
),
)
);
}
}

View file

@ -1,6 +1,5 @@
use std::fmt::Debug;
use diff;
use diff::Result::*;
use super::chunk::Chunk;
@ -12,7 +11,7 @@ pub enum Output<Item: Debug + PartialEq> {
}
impl<'a> Output<&'a str> {
pub fn to_strings(self) -> Output<String> {
pub fn into_strings(self) -> Output<String> {
match self {
Output::Resolved(x) => Output::Resolved(x.into_iter().map(str::to_string).collect()),
Output::Conflict(a, o, b) => Output::Conflict(
@ -27,10 +26,10 @@ impl<'a> Output<&'a str> {
fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
operations
.iter()
.filter_map(|x| match x {
&Both(y, _) => Some(y),
&Left(y) => Some(y),
&Right(_) => None,
.filter_map(|x| match *x {
Both(y, _) => Some(y),
Left(y) => Some(y),
Right(_) => None,
})
.collect()
}
@ -38,21 +37,16 @@ fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
fn choose_right<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
operations
.iter()
.filter_map(|x| match x {
&Both(_, y) => Some(y),
&Left(_) => None,
&Right(y) => Some(y),
.filter_map(|x| match *x {
Both(_, y) => Some(y),
Left(_) => None,
Right(y) => Some(y),
})
.collect()
}
fn no_change<Item>(operations: &[diff::Result<Item>]) -> bool {
operations
.iter()
.all(|x| match x {
&Both(..) => true,
_ => false,
})
operations.iter().all(|x| matches!(x, Both(..)))
}
pub fn resolve<'a, Item: 'a + Debug + PartialEq + Copy>(chunk: Chunk<'a, Item>) -> Output<Item> {
@ -69,92 +63,51 @@ pub fn resolve<'a, Item: 'a + Debug + PartialEq + Copy>(chunk: Chunk<'a, Item>)
return Output::Resolved(choose_right(chunk.0));
}
return Output::Conflict(
Output::Conflict(
choose_right(chunk.0),
choose_left(chunk.0),
choose_right(chunk.1),
);
)
}
#[cfg(test)]
mod test {
use diff::Result::*;
use super::*;
#[test]
fn empty() {
assert_eq!(
Output::Resolved(vec![]),
resolve::<i32>(Chunk(&[], &[]))
);
assert_eq!(Output::Resolved(vec![]), resolve::<i32>(Chunk(&[], &[])));
}
#[test]
fn same() {
assert_eq!(
Output::Resolved(vec![
1
]),
resolve::<i32>(Chunk(
&[Both(1, 1)],
&[Both(1, 1)]
))
Output::Resolved(vec![1]),
resolve::<i32>(Chunk(&[Both(1, 1)], &[Both(1, 1)]))
);
}
#[test]
fn only_left() {
assert_eq!(
Output::Resolved(vec![
2
]),
resolve::<i32>(Chunk(
&[
Left(1),
Right(2)
],
&[]
))
Output::Resolved(vec![2]),
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[]))
);
}
#[test]
fn false_conflict() {
assert_eq!(
Output::Resolved(vec![
2
]),
resolve::<i32>(Chunk(
&[
Left(1),
Right(2)
],
&[
Left(1),
Right(2)
],
))
Output::Resolved(vec![2]),
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[Left(1), Right(2)],))
);
}
#[test]
fn real_conflict() {
assert_eq!(
Output::Conflict(
vec![2],
vec![1],
vec![3],
),
resolve::<i32>(Chunk(
&[
Left(1),
Right(2)
],
&[
Left(1),
Right(3)
],
))
Output::Conflict(vec![2], vec![1], vec![3],),
resolve::<i32>(Chunk(&[Left(1), Right(2)], &[Left(1), Right(3)],))
);
}
}

View file

@ -1,4 +1,4 @@
use chrono;
use crate::theme::Theme;
fn slug_link(slug: &str) -> &str {
if slug.is_empty() {
@ -23,10 +23,14 @@ pub struct ArticleRevision {
pub latest: bool,
pub author: Option<String>,
pub theme: Theme,
}
impl ArticleRevision {
pub fn link(&self) -> &str { slug_link(&self.slug) }
pub fn link(&self) -> &str {
slug_link(&self.slug)
}
}
#[derive(Debug, PartialEq, Queryable)]
@ -43,10 +47,14 @@ pub struct ArticleRevisionStub {
pub latest: bool,
pub author: Option<String>,
pub theme: Theme,
}
impl ArticleRevisionStub {
pub fn link(&self) -> &str { slug_link(&self.slug) }
pub fn link(&self) -> &str {
slug_link(&self.slug)
}
}
use diesel::sql_types::Text;
@ -63,5 +71,7 @@ pub struct SearchResult {
}
impl SearchResult {
pub fn link(&self) -> &str { slug_link(&self.slug) }
pub fn link(&self) -> &str {
slug_link(&self.slug)
}
}

View file

@ -1,9 +1,18 @@
use pulldown_cmark::{Parser, Tag, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML};
use pulldown_cmark::Event::{Text, End};
use pulldown_cmark::Event::{End, Text};
use pulldown_cmark::{html, Parser, Tag, OPTION_DISABLE_HTML, OPTION_ENABLE_TABLES};
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 {
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
let p = Parser::new_ext(src, opts);
let p = parser(src);
let mut buf = String::new();
html::push_html(&mut buf, p);
buf
@ -14,22 +23,43 @@ fn is_html_special(c: char) -> bool {
}
pub fn render_markdown_for_fts(src: &str) -> String {
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
let p = Parser::new_ext(src, opts);
let p = parser(src);
let mut buf = String::new();
for event in p {
match event {
Text(text) =>
buf.push_str(&text.replace(is_html_special, " ")),
// As far as I understand this is a basic
// sanitizing to prevent HTML from
// appearing in page.
Text(text) => buf.push_str(&text.replace(is_html_special, " ")),
// Footnote links maybe?
End(Tag::Link(uri, _title)) => {
buf.push_str(" (");
buf.push_str(&uri.replace(is_html_special, " "));
buf.push_str(") ");
}
_ => buf.push_str(" "),
_ => buf.push(' '),
}
}
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);
}
}

View file

@ -1,12 +1,12 @@
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::server::*;
use build_config;
use mimes::*;
use site::Layout;
use web::{Resource, ResponseFuture};
use crate::build_config;
use crate::mimes::*;
use crate::site::system_page;
use crate::web::{Resource, ResponseFuture};
#[derive(Licenses)]
pub struct AboutResource;
@ -28,20 +28,20 @@ impl License {
fn link(&self) -> &'static str {
use self::License::*;
match self {
&Bsd3Clause => "bsd-3-clause",
&Mit => "mit",
&Mpl2 => "mpl2",
&Ofl11 => "sil-ofl-1.1",
Bsd3Clause => "bsd-3-clause",
Mit => "mit",
Mpl2 => "mpl2",
Ofl11 => "sil-ofl-1.1",
}
}
fn name(&self) -> &'static str {
use self::License::*;
match self {
&Bsd3Clause => "BSD-3-Clause",
&Mit => "MIT",
&Mpl2 => "MPL2",
&Ofl11 => "OFL-1.1",
Bsd3Clause => "BSD-3-Clause",
Mit => "MIT",
Mpl2 => "MPL2",
Ofl11 => "OFL-1.1",
}
}
}
@ -54,13 +54,15 @@ struct LicenseInfo {
}
#[derive(BartDisplay)]
#[template="templates/about.html"]
#[template = "templates/about.html"]
struct Template<'a> {
deps: &'a [LicenseInfo]
deps: &'a [LicenseInfo],
}
impl<'a> Template<'a> {
fn version(&self) -> &str { &build_config::VERSION }
fn version(&self) -> &str {
&build_config::VERSION
}
}
impl Resource for AboutResource {
@ -70,25 +72,27 @@ impl Resource for AboutResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
let head = self.head();
Box::new(head
.and_then(move |head| {
Ok(head
.with_body(Layout {
base: None, // Hmm, should perhaps accept `base` as argument
title: "About Sausagewiki",
body: &Template {
deps: &*LICENSE_INFOS
},
}.to_string()))
}))
Box::new(head.and_then(move |head| {
Ok(head.with_body(
system_page(
None, // Hmm, should perhaps accept `base` as argument
"About Sausagewiki",
Template {
deps: *LICENSE_INFOS,
},
)
.to_string(),
))
}))
}
}

View file

@ -1,22 +1,26 @@
use chrono::{TimeZone, DateTime, Local};
use chrono::{DateTime, Local, TimeZone};
use futures::{self, Future};
use hyper;
use hyper::header::{ContentType, Location};
use hyper::server::*;
use serde_json;
use serde_urlencoded;
use assets::ScriptJs;
use mimes::*;
use rendering::render_markdown;
use site::Layout;
use state::{State, UpdateResult, RebaseConflict};
use web::{Resource, ResponseFuture};
use crate::assets::ScriptJs;
use crate::mimes::*;
use crate::rendering::render_markdown;
use crate::site::Layout;
use crate::state::{RebaseConflict, State, UpdateResult};
use crate::theme::{self, Theme};
use crate::web::{Resource, ResponseFuture};
use super::changes_resource::QueryParameters;
struct SelectableTheme {
theme: Theme,
selected: bool,
}
#[derive(BartDisplay)]
#[template="templates/article.html"]
#[template = "templates/article.html"]
struct Template<'a> {
revision: i32,
last_updated: Option<&'a str>,
@ -26,11 +30,12 @@ struct Template<'a> {
title: &'a str,
raw: &'a str,
rendered: String,
themes: &'a [SelectableTheme],
}
impl<'a> Template<'a> {
fn script_js_checksum(&self) -> &'static str {
ScriptJs::checksum()
fn script_js(&self) -> &'static str {
ScriptJs::resource_name()
}
}
@ -39,6 +44,7 @@ struct UpdateArticle {
base_revision: i32,
title: String,
body: String,
theme: Option<Theme>,
}
pub struct ArticleResource {
@ -50,7 +56,12 @@ pub struct ArticleResource {
impl ArticleResource {
pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self {
Self { state, article_id, revision, edit }
Self {
state,
article_id,
revision,
edit,
}
}
}
@ -70,12 +81,23 @@ pub fn last_updated(article_id: i32, created: &DateTime<Local>, author: Option<&
Template {
created: &created.to_rfc2822(),
article_history: &format!("_changes{}", QueryParameters::default().article_id(Some(article_id)).into_link()),
article_history: &format!(
"_changes{}",
QueryParameters::default()
.article_id(Some(article_id))
.into_link()
),
author: author.map(|author| Author {
author: &author,
history: format!("_changes{}", QueryParameters::default().author(Some(author.to_owned())).into_link()),
author,
history: format!(
"_changes{}",
QueryParameters::default()
.author(Some(author.to_owned()))
.into_link()
),
}),
}.to_string()
}
.to_string()
}
impl Resource for ArticleResource {
@ -85,38 +107,50 @@ impl Resource for ArticleResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
let data = self.state.get_article_revision(self.article_id, self.revision)
let data = self
.state
.get_article_revision(self.article_id, self.revision)
.map(|x| x.expect("Data model guarantees that this exists"));
let head = self.head();
Box::new(data.join(head)
.and_then(move |(data, head)| {
Ok(head
.with_body(Layout {
base: None, // Hmm, should perhaps accept `base` as argument
Box::new(data.join(head).and_then(move |(data, head)| {
Ok(head.with_body(
Layout {
base: None, // Hmm, should perhaps accept `base` as argument
title: &data.title,
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,
body: &Template {
revision: data.revision,
last_updated: Some(&last_updated(
data.article_id,
&Local.from_utc_datetime(&data.created),
data.author.as_ref().map(|x| &**x)
)),
edit: self.edit,
cancel_url: Some(data.link()),
title: &data.title,
raw: &data.body,
rendered: render_markdown(&data.body),
},
}.to_string()))
}))
raw: &data.body,
rendered: render_markdown(&data.body),
themes: &theme::THEMES
.iter()
.map(|&x| SelectableTheme {
theme: x,
selected: x == data.theme,
})
.collect::<Vec<_>>(),
},
}
.to_string(),
))
}))
}
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
@ -125,7 +159,7 @@ impl Resource for ArticleResource {
use futures::Stream;
#[derive(BartDisplay)]
#[template="templates/article_contents.html"]
#[template = "templates/article_contents.html"]
struct Template<'a> {
title: &'a str,
rendered: String,
@ -138,69 +172,84 @@ impl Resource for ArticleResource {
revision: i32,
title: &'a str,
body: &'a str,
theme: Theme,
rendered: &'a str,
last_updated: &'a str,
}
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |update: UpdateArticle| {
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
})
.and_then(|updated| match updated {
UpdateResult::Success(updated) =>
Ok(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse {
conflict: false,
slug: &updated.slug,
revision: updated.revision,
title: &updated.title,
body: &updated.body,
rendered: &Template {
title: &updated.title,
rendered: render_markdown(&updated.body),
}.to_string(),
last_updated: &last_updated(
updated.article_id,
&Local.from_utc_datetime(&updated.created),
updated.author.as_ref().map(|x| &**x)
),
}).expect("Should never fail"))
),
UpdateResult::RebaseConflict(RebaseConflict {
base_article, title, body
}) => {
let title = title.flatten();
let body = body.flatten();
Ok(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse {
conflict: true,
slug: &base_article.slug,
revision: base_article.revision,
title: &title,
body: &body,
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_ref().map(|x| &**x)
),
}).expect("Should never fail"))
Box::new(
body.concat2()
.map_err(Into::into)
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
.and_then(move |update: UpdateArticle| {
self.state.update_article(
self.article_id,
update.base_revision,
update.title,
update.body,
identity,
update.theme,
)
}
})
})
.and_then(|updated| match updated {
UpdateResult::Success(updated) => Ok(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(
serde_json::to_string(&PutResponse {
conflict: false,
slug: &updated.slug,
revision: updated.revision,
title: &updated.title,
body: &updated.body,
theme: updated.theme,
rendered: &Template {
title: &updated.title,
rendered: render_markdown(&updated.body),
}
.to_string(),
last_updated: &last_updated(
updated.article_id,
&Local.from_utc_datetime(&updated.created),
updated.author.as_deref(),
),
})
.expect("Should never fail"),
)),
UpdateResult::RebaseConflict(RebaseConflict {
base_article,
title,
body,
theme,
}) => {
let title = title.flatten();
let body = body.flatten();
Ok(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(
serde_json::to_string(&PutResponse {
conflict: true,
slug: &base_article.slug,
revision: base_article.revision,
title: &title,
body: &body,
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"),
))
}
}),
)
}
@ -209,53 +258,67 @@ impl Resource for ArticleResource {
use futures::Stream;
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |update: UpdateArticle| {
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
})
.and_then(|updated| {
match updated {
Box::new(
body.concat2()
.map_err(Into::into)
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
.and_then(move |update: UpdateArticle| {
self.state.update_article(
self.article_id,
update.base_revision,
update.title,
update.body,
identity,
update.theme,
)
})
.and_then(|updated| match updated {
UpdateResult::Success(updated) => Ok(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")
),
.with_body("See other")),
UpdateResult::RebaseConflict(RebaseConflict {
base_article, title, body
base_article,
title,
body,
theme,
}) => {
let title = title.flatten();
let body = body.flatten();
Ok(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
.with_body(Layout {
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()),
.with_body(
Layout {
base: None,
title: &title,
raw: &body,
rendered: render_markdown(&body),
},
}.to_string())
)
theme,
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_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(),
))
}
}
})
}),
)
}
}

View file

@ -1,14 +1,14 @@
use chrono::{TimeZone, DateTime, Local};
use chrono::{DateTime, Local, TimeZone};
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::server::*;
use mimes::*;
use models;
use rendering::render_markdown;
use site::Layout;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::models;
use crate::rendering::render_markdown;
use crate::site::system_page;
use crate::web::{Resource, ResponseFuture};
use super::changes_resource::QueryParameters;
use super::diff_resource;
@ -24,7 +24,12 @@ impl ArticleRevisionResource {
}
}
pub fn timestamp_and_author(sequence_number: i32, article_id: i32, created: &DateTime<Local>, author: Option<&str>) -> String {
pub fn timestamp_and_author(
sequence_number: i32,
article_id: i32,
created: &DateTime<Local>,
author: Option<&str>,
) -> String {
struct Author<'a> {
author: &'a str,
history: String,
@ -42,15 +47,17 @@ pub fn timestamp_and_author(sequence_number: i32, article_id: i32, created: &Dat
Template {
created: &created.to_rfc2822(),
article_history: &format!("_changes{}",
article_history: &format!(
"_changes{}",
QueryParameters::default()
.pagination(pagination)
.article_id(Some(article_id))
.into_link()
),
author: author.map(|author| Author {
author: &author,
history: format!("_changes{}",
author,
history: format!(
"_changes{}",
QueryParameters::default()
.pagination(pagination)
.article_id(Some(article_id))
@ -58,7 +65,8 @@ pub fn timestamp_and_author(sequence_number: i32, article_id: i32, created: &Dat
.into_link()
),
}),
}.to_string()
}
.to_string()
}
impl Resource for ArticleRevisionResource {
@ -68,57 +76,56 @@ impl Resource for ArticleRevisionResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
#[derive(BartDisplay)]
#[template="templates/article_revision.html"]
#[template = "templates/article_revision.html"]
struct Template<'a> {
link_current: &'a str,
timestamp_and_author: &'a str,
diff_link: Option<String>,
title: &'a str,
rendered: String,
}
let head = self.head();
let data = self.data;
Box::new(head
.and_then(move |head|
Ok(head
.with_body(Layout {
base: Some("../../"), // Hmm, should perhaps accept `base` as argument
title: &data.title,
body: &Template {
link_current: &format!("_by_id/{}", data.article_id),
timestamp_and_author: &timestamp_and_author(
data.sequence_number,
Box::new(head.and_then(move |head| {
Ok(head.with_body(
system_page(
Some("../../"), // Hmm, should perhaps accept `base` as argument
&data.title,
&Template {
link_current: &format!("_by_id/{}", data.article_id),
timestamp_and_author: &timestamp_and_author(
data.sequence_number,
data.article_id,
&Local.from_utc_datetime(&data.created),
data.author.as_deref(),
),
diff_link: if data.revision > 1 {
Some(format!(
"_diff/{}?{}",
data.article_id,
&Local.from_utc_datetime(&data.created),
data.author.as_ref().map(|x| &**x)
),
diff_link:
if data.revision > 1 {
Some(format!("_diff/{}?{}",
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),
diff_resource::QueryParameters::new(
data.revision as u32 - 1,
data.revision as u32,
)
))
} else {
None
},
}.to_string()))
rendered: render_markdown(&data.body),
},
)
.to_string(),
))
}))
}
}

View file

@ -1,16 +1,14 @@
use diesel;
use futures::{self, Future};
use futures::future::{done, finished};
use hyper;
use futures::{self, Future};
use hyper::header::ContentType;
use hyper::server::*;
use serde_urlencoded;
use mimes::*;
use schema::article_revisions;
use site::Layout;
use state::State;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::schema::article_revisions;
use crate::site::system_page;
use crate::state::State;
use crate::web::{Resource, ResponseFuture};
use super::diff_resource;
use super::pagination::Pagination;
@ -18,7 +16,7 @@ use super::TemporaryRedirectResource;
const DEFAULT_LIMIT: i32 = 30;
type BoxResource = Box<Resource + Sync + Send>;
type BoxResource = Box<dyn Resource + Sync + Send>;
#[derive(Clone)]
pub struct ChangesLookup {
@ -40,8 +38,16 @@ pub struct QueryParameters {
impl QueryParameters {
pub fn pagination(self, pagination: Pagination<i32>) -> Self {
Self {
after: if let Pagination::After(x) = pagination { Some(x) } else { None },
before: if let Pagination::Before(x) = pagination { Some(x) } else { None },
after: if let Pagination::After(x) = pagination {
Some(x)
} else {
None
},
before: if let Pagination::Before(x) = pagination {
Some(x)
} else {
None
},
..self
}
}
@ -56,14 +62,18 @@ impl QueryParameters {
pub fn limit(self, limit: i32) -> Self {
Self {
limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None },
limit: if limit != DEFAULT_LIMIT {
Some(limit)
} else {
None
},
..self
}
}
pub fn into_link(self) -> String {
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
if args.len() > 0 {
if !args.is_empty() {
format!("?{}", args)
} else {
"_changes".to_owned()
@ -71,14 +81,12 @@ impl QueryParameters {
}
}
fn apply_query_config<'a>(
mut query: article_revisions::BoxedQuery<'a, diesel::sqlite::Sqlite>,
fn apply_query_config(
mut query: article_revisions::BoxedQuery<diesel::sqlite::Sqlite>,
article_id: Option<i32>,
author: Option<String>,
limit: i32,
)
-> article_revisions::BoxedQuery<'a, diesel::sqlite::Sqlite>
{
) -> article_revisions::BoxedQuery<diesel::sqlite::Sqlite> {
use diesel::prelude::*;
if let Some(article_id) = article_id {
@ -94,10 +102,16 @@ fn apply_query_config<'a>(
impl ChangesLookup {
pub fn new(state: State, show_authors: bool) -> ChangesLookup {
Self { state, show_authors }
Self {
state,
show_authors,
}
}
pub fn lookup(&self, query: Option<&str>) -> Box<Future<Item=Option<BoxResource>, Error=::web::Error>> {
pub fn lookup(
&self,
query: Option<&str>,
) -> Box<dyn Future<Item = Option<BoxResource>, Error = crate::web::Error>> {
use super::pagination;
let state = self.state.clone();
@ -111,54 +125,80 @@ impl ChangesLookup {
let limit = match params.limit {
None => Ok(DEFAULT_LIMIT),
Some(x) if 1 <= x && x <= 100 => Ok(x),
Some(x) if (1..=100).contains(&x) => Ok(x),
_ => Err("`limit` argument must be in range [1, 100]"),
}?;
Ok((pagination, params.article_id, params.author, limit))
})())
.and_then(move |(pagination, article_id, author, limit)| match pagination {
Pagination::After(x) => {
let author2 = author.clone();
.and_then(move |(pagination, article_id, author, limit)| {
match pagination {
Pagination::After(x) => {
let author2 = author.clone();
Box::new(state.query_article_revision_stubs(move |query| {
use diesel::prelude::*;
Box::new(
state
.query_article_revision_stubs(move |query| {
use diesel::prelude::*;
apply_query_config(query, article_id, author2, limit)
.filter(article_revisions::sequence_number.gt(x))
.order(article_revisions::sequence_number.asc())
}).and_then(move |mut data| {
let extra_element = if data.len() > limit as usize {
data.pop()
} else {
None
};
apply_query_config(query, article_id, author2, limit)
.filter(article_revisions::sequence_number.gt(x))
.order(article_revisions::sequence_number.asc())
})
.and_then(move |mut data| {
let extra_element = if data.len() > limit as usize {
data.pop()
} else {
None
};
let args =
QueryParameters {
after: None,
before: None,
article_id,
author,
limit: None,
}
.limit(limit);
let args = QueryParameters {
after: None,
before: None,
article_id,
author,
limit: None,
}
.limit(limit);
Ok(Some(match extra_element {
Some(x) => Box::new(TemporaryRedirectResource::new(
args
.pagination(Pagination::Before(x.sequence_number))
.into_link()
)) as BoxResource,
None => Box::new(TemporaryRedirectResource::new(
args.into_link()
)) 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))),
Pagination::None => Box::new(finished(Some(Box::new(ChangesResource::new(state, show_authors, None, article_id, author, limit)) as BoxResource))),
})
Ok(Some(match extra_element {
Some(x) => Box::new(TemporaryRedirectResource::new(
args.pagination(Pagination::Before(x.sequence_number))
.into_link(),
))
as BoxResource,
None => Box::new(TemporaryRedirectResource::new(
args.into_link(),
))
as BoxResource,
}))
}),
)
as Box<
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))),
}
}),
)
}
}
@ -173,8 +213,22 @@ pub struct ChangesResource {
}
impl ChangesResource {
pub fn new(state: State, 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 }
pub fn new(
state: State,
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 {
@ -196,14 +250,15 @@ impl Resource for ChangesResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
use chrono::{TimeZone, Local};
use chrono::{Local, TimeZone};
struct Row<'a> {
resource: &'a ChangesResource,
@ -224,7 +279,8 @@ impl Resource for ChangesResource {
impl<'a> Row<'a> {
fn author_link(&self) -> String {
self.resource.query_args()
self.resource
.query_args()
.pagination(Pagination::After(self.sequence_number))
.author(self.author.clone())
.into_link()
@ -237,7 +293,7 @@ impl Resource for ChangesResource {
}
#[derive(BartDisplay)]
#[template="templates/changes.html"]
#[template = "templates/changes.html"]
struct Template<'a> {
resource: &'a ChangesResource,
@ -251,7 +307,7 @@ impl Resource for ChangesResource {
fn subject_clause(&self) -> String {
match self.resource.article_id {
Some(x) => format!(" <a href=\"_by_id/{}\">this article</a>", x),
None => format!(" the wiki"),
None => " the wiki".to_string(),
}
}
@ -260,24 +316,25 @@ impl Resource for ChangesResource {
}
fn all_articles_link(&self) -> Option<String> {
self.resource.article_id.map(|_| {
self.resource.query_args()
.article_id(None)
.into_link()
})
self.resource
.article_id
.map(|_| self.resource.query_args().article_id(None).into_link())
}
fn all_authors_link(&self) -> Option<String> {
self.resource.author.as_ref().map(|_| {
self.resource.query_args()
.author(None)
.into_link()
})
self.resource
.author
.as_ref()
.map(|_| self.resource.query_args().author(None).into_link())
}
}
let (before, article_id, author, limit) =
(self.before.clone(), self.article_id.clone(), self.author.clone(), self.limit);
let (before, article_id, author, limit) = (
self.before,
self.article_id,
self.author.clone(),
self.limit,
);
let data = self.state.query_article_revision_stubs(move |query| {
use diesel::prelude::*;
@ -292,78 +349,89 @@ impl Resource for ChangesResource {
let head = self.head();
Box::new(data.join(head)
.and_then(move |(mut data, head)| {
use std::iter::Iterator;
Box::new(data.join(head).and_then(move |(mut data, head)| {
let extra_element = if data.len() > self.limit as usize {
data.pop()
} else {
None
};
let extra_element = if data.len() > self.limit as usize {
data.pop()
} else {
None
};
let (newer, older) = match self.before {
Some(x) => (
Some(NavLinks {
more: self
.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 (newer, older) = match self.before {
Some(x) => (
Some(NavLinks {
more: self.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
.into_iter()
.map(|x| Row {
resource: &self,
sequence_number: x.sequence_number,
article_id: x.article_id,
revision: x.revision,
created: Local.from_utc_datetime(&x.created).to_rfc2822(),
author: x.author,
_slug: x.slug,
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<_>>();
let changes = &data.into_iter().map(|x| {
Row {
Ok(head.with_body(
system_page(
None, // Hmm, should perhaps accept `base` as argument
"Changes",
Template {
resource: &self,
sequence_number: x.sequence_number,
article_id: x.article_id,
revision: x.revision,
created: Local.from_utc_datetime(&x.created).to_rfc2822(),
author: x.author,
_slug: x.slug,
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()))
}))
show_authors: self.show_authors,
newer,
older,
changes,
},
)
.to_string(),
))
}))
}
}

View file

@ -1,23 +1,22 @@
use std::fmt;
use diff;
use futures::{self, Future};
use futures::future::done;
use hyper;
use futures::{self, Future};
use hyper::header::ContentType;
use hyper::server::*;
use serde_urlencoded;
use mimes::*;
use models::ArticleRevision;
use site::Layout;
use state::State;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::models::ArticleRevision;
use crate::site::Layout;
use crate::state::State;
use crate::theme;
use crate::web::{Resource, ResponseFuture};
use super::changes_resource;
use super::pagination::Pagination;
type BoxResource = Box<Resource + Sync + Send>;
type BoxResource = Box<dyn Resource + Sync + Send>;
#[derive(Clone)]
pub struct DiffLookup {
@ -47,25 +46,28 @@ impl DiffLookup {
Self { state }
}
pub fn lookup(&self, article_id: u32, query: Option<&str>) -> Box<Future<Item=Option<BoxResource>, Error=::web::Error>> {
pub fn lookup(
&self,
article_id: u32,
query: Option<&str>,
) -> Box<dyn Future<Item = Option<BoxResource>, Error = crate::web::Error>> {
let state = self.state.clone();
Box::new(done(
serde_urlencoded::from_str(query.unwrap_or(""))
.map_err(Into::into)
).and_then(move |params: QueryParameters| {
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);
Box::new(
done(serde_urlencoded::from_str(query.unwrap_or("")).map_err(Into::into))
.and_then(move |params: QueryParameters| {
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)
}).and_then(move |(from, to)| {
match (from, to) {
(Some(from), Some(to)) =>
Ok(Some(Box::new(DiffResource::new(from, to)) as BoxResource)),
_ =>
Ok(None),
}
}))
from.join(to)
})
.and_then(move |(from, to)| match (from, to) {
(Some(from), Some(to)) => {
Ok(Some(Box::new(DiffResource::new(from, to)) as BoxResource))
}
_ => Ok(None),
}),
)
}
}
@ -88,9 +90,10 @@ impl Resource for DiffResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
@ -100,6 +103,8 @@ impl Resource for DiffResource {
struct Template<'a> {
consecutive: bool,
article_id: u32,
author: Option<&'a str>,
author_link: &'a str,
article_history_link: &'a str,
from_link: &'a str,
to_link: &'a str,
@ -116,41 +121,88 @@ impl Resource for DiffResource {
let head = self.head();
Box::new(head
.and_then(move |head| {
Ok(head
.with_body(Layout {
base: Some("../"), // Hmm, should perhaps accept `base` as argument
title: "Difference",
body: &Template {
consecutive: self.to.revision - self.from.revision == 1,
article_id: self.from.article_id as u32,
article_history_link: &format!("_changes{}",
changes_resource::QueryParameters::default()
.article_id(Some(self.from.article_id))
.pagination(Pagination::After(self.from.revision))
.into_link()
),
from_link: &format!("_revisions/{}/{}", self.from.article_id, self.from.revision),
to_link: &format!("_revisions/{}/{}", self.to.article_id, self.to.revision),
title: &diff::chars(&self.from.title, &self.to.title)
.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<_>>(),
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<_>>()
},
}.to_string()))
}))
Box::new(head.and_then(move |head| {
let consecutive = self.to.revision - self.from.revision == 1;
let author = match consecutive {
true => self.to.author.as_deref(),
false => None,
};
let author_link = &format!(
"_changes{}",
changes_resource::QueryParameters::default()
.author(author.map(|x| x.to_owned()))
.pagination(Pagination::After(self.from.sequence_number))
.into_link()
);
let article_history_link = &format!(
"_changes{}",
changes_resource::QueryParameters::default()
.article_id(Some(self.from.article_id))
.pagination(Pagination::After(self.from.sequence_number))
.into_link()
);
let title = &diff::chars(&self.from.title, &self.to.title)
.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<_>>();
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(),
))
}))
}
}

View file

@ -1,11 +1,11 @@
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::server::*;
use mimes::*;
use site::Layout;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::site::system_page;
use crate::web::{Resource, ResponseFuture};
pub struct HtmlResource {
base: Option<&'static str>,
@ -15,17 +15,14 @@ pub struct HtmlResource {
impl HtmlResource {
pub fn new(base: Option<&'static str>, title: &'static str, html_body: &'static str) -> Self {
HtmlResource { base, title, html_body }
HtmlResource {
base,
title,
html_body,
}
}
}
#[derive(BartDisplay)]
#[template="templates/simple.html"]
struct Template<'a> {
title: &'a str,
html_body: &'a str,
}
impl Resource for HtmlResource {
fn allow(&self) -> Vec<hyper::Method> {
use hyper::Method::*;
@ -33,26 +30,18 @@ impl Resource for HtmlResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
let head = self.head();
Box::new(head
.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()))
}))
Box::new(head.and_then(move |head| {
Ok(head.with_body(system_page(self.base, self.title, self.html_body).to_string()))
}))
}
}

View file

@ -1,23 +1,25 @@
pub mod pagination;
mod about_resource;
mod article_revision_resource;
mod article_resource;
mod article_revision_resource;
mod changes_resource;
mod diff_resource;
mod html_resource;
mod new_article_resource;
mod read_only_resource;
mod search_resource;
mod sitemap_resource;
mod temporary_redirect_resource;
pub use self::about_resource::AboutResource;
pub use self::article_revision_resource::ArticleRevisionResource;
pub use self::article_resource::ArticleResource;
pub use self::article_revision_resource::ArticleRevisionResource;
pub use self::changes_resource::{ChangesLookup, ChangesResource};
pub use self::diff_resource::{DiffLookup, DiffResource};
pub use self::html_resource::HtmlResource;
pub use self::new_article_resource::NewArticleResource;
pub use self::read_only_resource::ReadOnlyResource;
pub use self::search_resource::SearchLookup;
pub use self::sitemap_resource::SitemapResource;
pub use self::temporary_redirect_resource::TemporaryRedirectResource;

View file

@ -1,16 +1,15 @@
use futures::{self, Future};
use hyper;
use hyper::header::{ContentType, Location};
use hyper::server::*;
use serde_json;
use serde_urlencoded;
use assets::ScriptJs;
use mimes::*;
use rendering::render_markdown;
use site::Layout;
use state::State;
use web::{Resource, ResponseFuture};
use crate::assets::ScriptJs;
use crate::mimes::*;
use crate::rendering::render_markdown;
use crate::site::Layout;
use crate::state::State;
use crate::theme::{self, Theme};
use crate::web::{Resource, ResponseFuture};
const NEW: &str = "NEW";
@ -27,6 +26,7 @@ fn title_from_slug(slug: &str) -> String {
pub struct NewArticleResource {
state: State,
slug: Option<String>,
edit: bool,
}
#[derive(Deserialize)]
@ -34,11 +34,12 @@ struct CreateArticle {
base_revision: String,
title: String,
body: String,
theme: Option<Theme>,
}
impl NewArticleResource {
pub fn new(state: State, slug: Option<String>) -> Self {
Self { state, slug }
pub fn new(state: State, slug: Option<String>, edit: bool) -> Self {
Self { state, slug, edit }
}
}
@ -49,15 +50,22 @@ impl Resource for NewArticleResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::NotFound)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::NotFound)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
// TODO Remove duplication with article_resource.rs:
struct SelectableTheme {
theme: Theme,
selected: bool,
}
#[derive(BartDisplay)]
#[template="templates/article.html"]
#[template = "templates/article.html"]
struct Template<'a> {
revision: &'a str,
last_updated: Option<&'a str>,
@ -67,48 +75,56 @@ impl Resource for NewArticleResource {
title: &'a str,
raw: &'a str,
rendered: &'a str,
themes: &'a [SelectableTheme],
}
impl<'a> Template<'a> {
fn script_js_checksum(&self) -> &'static str {
ScriptJs::checksum()
fn script_js(&self) -> &'static str {
ScriptJs::resource_name()
}
}
let title = self.slug.as_ref()
let title = self
.slug
.as_ref()
.map_or("".to_owned(), |x| title_from_slug(x));
Box::new(self.head()
.and_then(move |head| {
Ok(head
.with_body(Layout {
base: None, // Hmm, should perhaps accept `base` as argument
Box::new(self.head().and_then(move |head| {
Ok(head.with_body(
Layout {
base: None, // Hmm, should perhaps accept `base` as argument
title: &title,
theme: theme::Theme::Gray,
body: &Template {
revision: NEW,
last_updated: None,
edit: self.edit,
cancel_url: self.slug.as_deref(),
title: &title,
body: &Template {
revision: NEW,
last_updated: None,
// Implicitly start in edit-mode when no slug is given. This
// currently directly corresponds to the /_new endpoint
edit: self.slug.is_none(),
cancel_url: self.slug.as_ref().map(|x| &**x),
title: &title,
raw: "",
rendered: EMPTY_ARTICLE_MESSAGE,
},
}.to_string()))
}))
raw: "",
rendered: EMPTY_ARTICLE_MESSAGE,
themes: &theme::THEMES
.iter()
.map(|&x| SelectableTheme {
theme: x,
selected: false,
})
.collect::<Vec<_>>(),
},
}
.to_string(),
))
}))
}
fn put(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
// TODO Check incoming Content-Type
// TODO Refactor? Reduce duplication with ArticleResource::put?
use chrono::{TimeZone, Local};
use chrono::{Local, TimeZone};
use futures::Stream;
#[derive(BartDisplay)]
#[template="templates/article_contents.html"]
#[template = "templates/article_contents.html"]
struct Template<'a> {
title: &'a str,
rendered: String,
@ -121,45 +137,56 @@ impl Resource for NewArticleResource {
revision: i32,
title: &'a str,
body: &'a str,
theme: Theme,
rendered: &'a str,
last_updated: &'a str,
}
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |arg: CreateArticle| {
if arg.base_revision != NEW {
unimplemented!("Version update conflict");
}
self.state.create_article(self.slug.clone(), arg.title, arg.body, identity)
})
.and_then(|updated| {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse {
slug: &updated.slug,
article_id: updated.article_id,
revision: updated.revision,
title: &updated.title,
body: &updated.body,
rendered: &Template {
title: &updated.title,
rendered: render_markdown(&updated.body),
}.to_string(),
last_updated: &super::article_resource::last_updated(
updated.article_id,
&Local.from_utc_datetime(&updated.created),
updated.author.as_ref().map(|x| &**x)
),
}).expect("Should never fail"))
)
})
Box::new(
body.concat2()
.map_err(Into::into)
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
.and_then(move |arg: CreateArticle| {
if arg.base_revision != NEW {
unimplemented!("Version update conflict");
}
let theme = arg.theme.unwrap_or_else(theme::random);
self.state.create_article(
self.slug.clone(),
arg.title,
arg.body,
identity,
theme,
)
})
.and_then(|updated| {
futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(
serde_json::to_string(&PutResponse {
slug: &updated.slug,
article_id: updated.article_id,
revision: updated.revision,
title: &updated.title,
body: &updated.body,
theme: updated.theme,
rendered: &Template {
title: &updated.title,
rendered: render_markdown(&updated.body),
}
.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"),
),
)
}),
)
}
@ -169,27 +196,32 @@ impl Resource for NewArticleResource {
use futures::Stream;
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |arg: CreateArticle| {
if arg.base_revision != NEW {
unimplemented!("Version update conflict");
}
self.state.create_article(self.slug.clone(), arg.title, arg.body, identity)
})
.and_then(|updated| {
futures::finished(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")
)
})
Box::new(
body.concat2()
.map_err(Into::into)
.and_then(|body| serde_urlencoded::from_bytes(&body).map_err(Into::into))
.and_then(move |arg: CreateArticle| {
if arg.base_revision != NEW {
unimplemented!("Version update conflict");
}
let theme = arg.theme.unwrap_or_else(theme::random);
self.state.create_article(
self.slug.clone(),
arg.title,
arg.body,
identity,
theme,
)
})
.and_then(|updated| {
futures::finished(
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"),
)
}),
)
}
}

View file

@ -8,15 +8,11 @@ pub struct Error;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{}", (self as &error::Error).description())
write!(f, "`after` and `before` are mutually exclusive")
}
}
impl error::Error for Error {
fn description(&self) -> &str {
"`after` and `before` are mutually exclusive"
}
}
impl error::Error for Error {}
#[derive(Deserialize)]
struct PaginationStruct<T> {
@ -37,16 +33,16 @@ impl<T> PaginationStruct<T> {
(Some(x), None) => Ok(Pagination::After(x)),
(None, Some(x)) => Ok(Pagination::Before(x)),
(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> {
let pagination: PaginationStruct<T> = serde_urlencoded::from_str(s).map_err(|_| Error)?; // TODO Proper error reporting
Ok(pagination.into_enum()?)
pagination.into_enum()
}
pub fn from_fields<T>(after: Option<T>, before: Option<T>) -> Result<Pagination<T>, Error> {
Ok(PaginationStruct { after, before }.into_enum()?)
PaginationStruct { after, before }.into_enum()
}

View file

@ -0,0 +1,38 @@
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())
}))
}
}

View file

@ -1,20 +1,18 @@
use futures::{self, Future};
use hyper;
use hyper::header::{Accept, ContentType};
use hyper::server::*;
use serde_json;
use serde_urlencoded;
use mimes::*;
use models::SearchResult;
use site::Layout;
use state::State;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::models::SearchResult;
use crate::site::system_page;
use crate::state::State;
use crate::web::{Resource, ResponseFuture};
const DEFAULT_LIMIT: u32 = 10;
const DEFAULT_SNIPPET_SIZE: u32 = 30;
type BoxResource = Box<Resource + Sync + Send>;
type BoxResource = Box<dyn Resource + Sync + Send>;
#[derive(Serialize, Deserialize, Default)]
pub struct QueryParameters {
@ -34,21 +32,29 @@ impl QueryParameters {
pub fn limit(self, limit: u32) -> Self {
Self {
limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None },
limit: if limit != DEFAULT_LIMIT {
Some(limit)
} else {
None
},
..self
}
}
pub fn snippet_size(self, snippet_size: u32) -> Self {
Self {
snippet_size: if snippet_size != DEFAULT_SNIPPET_SIZE { Some(snippet_size) } else { None },
snippet_size: if snippet_size != DEFAULT_SNIPPET_SIZE {
Some(snippet_size)
} else {
None
},
..self
}
}
pub fn into_link(self) -> String {
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
if args.len() > 0 {
if !args.is_empty() {
format!("_search?{}", args)
} else {
"_search".to_owned()
@ -66,18 +72,16 @@ impl SearchLookup {
Self { state }
}
pub fn lookup(&self, query: Option<&str>) -> Result<Option<BoxResource>, ::web::Error> {
pub fn lookup(&self, query: Option<&str>) -> Result<Option<BoxResource>, crate::web::Error> {
let args: QueryParameters = serde_urlencoded::from_str(query.unwrap_or(""))?;
Ok(Some(Box::new(
SearchResource::new(
self.state.clone(),
args.q,
args.limit.unwrap_or(DEFAULT_LIMIT),
args.offset.unwrap_or(0),
args.snippet_size.unwrap_or(DEFAULT_SNIPPET_SIZE),
)
)))
Ok(Some(Box::new(SearchResource::new(
self.state.clone(),
args.q,
args.limit.unwrap_or(DEFAULT_LIMIT),
args.offset.unwrap_or(0),
args.snippet_size.unwrap_or(DEFAULT_SNIPPET_SIZE),
))))
}
}
@ -98,8 +102,21 @@ pub enum ResponseType {
}
impl SearchResource {
pub fn new(state: State, query: Option<String>, limit: u32, offset: u32, snippet_size: u32) -> Self {
Self { state, response_type: ResponseType::Html, query, limit, offset, snippet_size }
pub fn new(
state: State,
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 {
@ -107,9 +124,9 @@ impl SearchResource {
q: self.query.clone(),
..QueryParameters::default()
}
.offset(self.offset)
.limit(self.limit)
.snippet_size(self.snippet_size)
.offset(self.offset)
.limit(self.limit)
.snippet_size(self.snippet_size)
}
}
@ -126,21 +143,24 @@ impl Resource for SearchResource {
self.response_type = match accept.first() {
Some(&QualityItem { item: ref mime, .. })
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON
=> ResponseType::Json,
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON =>
{
ResponseType::Json
}
_ => ResponseType::Html,
};
}
fn head(&self) -> ResponseFuture {
let content_type = match &self.response_type {
&ResponseType::Json => ContentType(APPLICATION_JSON.clone()),
&ResponseType::Html => ContentType(TEXT_HTML.clone()),
let content_type = match self.response_type {
ResponseType::Json => ContentType(APPLICATION_JSON.clone()),
ResponseType::Html => ContentType(TEXT_HTML.clone()),
};
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(content_type)
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(content_type),
))
}
@ -154,7 +174,7 @@ impl Resource for SearchResource {
}
#[derive(BartDisplay)]
#[template="templates/search.html"]
#[template = "templates/search.html"]
struct Template<'a> {
query: &'a str,
hits: &'a [(usize, &'a SearchResult)],
@ -163,55 +183,66 @@ impl Resource for SearchResource {
}
// TODO: Show a search "front page" when no query is given:
let query = self.query.as_ref().map(|x| x.clone()).unwrap_or("".to_owned());
let query = self
.query
.as_ref()
.cloned()
.unwrap_or_else(|| "".to_owned());
let data = self.state.search_query(query, (self.limit + 1) as i32, self.offset as i32, self.snippet_size as i32);
let data = self.state.search_query(
query,
(self.limit + 1) as i32,
self.offset as i32,
self.snippet_size as i32,
);
let head = self.head();
Box::new(data.join(head)
.and_then(move |(mut data, head)| {
let prev = if self.offset > 0 {
Some(self.query_args()
Box::new(data.join(head).and_then(move |(mut data, head)| {
let prev = if self.offset > 0 {
Some(
self.query_args()
.offset(self.offset.saturating_sub(self.limit))
.into_link()
)
} else {
None
};
.into_link(),
)
} else {
None
};
let next = if data.len() > self.limit as usize {
data.pop();
Some(self.query_args()
let next = if data.len() > self.limit as usize {
data.pop();
Some(
self.query_args()
.offset(self.offset + self.limit)
.into_link()
)
} else {
None
};
.into_link(),
)
} else {
None
};
match &self.response_type {
&ResponseType::Json => Ok(head
.with_body(serde_json::to_string(&JsonResponse {
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
hits: &data,
match self.response_type {
ResponseType::Json => Ok(head.with_body(
serde_json::to_string(&JsonResponse {
query: self.query.as_deref().unwrap_or(""),
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,
next,
}).expect("Should never fail"))
),
&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())),
}
}))
},
)
.to_string(),
)),
}
}))
}
}

View file

@ -1,13 +1,13 @@
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::server::*;
use mimes::*;
use models::ArticleRevisionStub;
use site::Layout;
use state::State;
use web::{Resource, ResponseFuture};
use crate::mimes::*;
use crate::models::ArticleRevisionStub;
use crate::site::system_page;
use crate::state::State;
use crate::web::{Resource, ResponseFuture};
pub struct SitemapResource {
state: State,
@ -26,15 +26,16 @@ impl Resource for SitemapResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
#[derive(BartDisplay)]
#[template="templates/sitemap.html"]
#[template = "templates/sitemap.html"]
struct Template<'a> {
articles: &'a [ArticleRevisionStub],
}
@ -42,16 +43,17 @@ impl Resource for SitemapResource {
let data = self.state.get_latest_article_revision_stubs();
let head = self.head();
Box::new(data.join(head)
.and_then(move |(articles, head)| {
Ok(head
.with_body(Layout {
base: None, // Hmm, should perhaps accept `base` as argument
title: "Sitemap",
body: &Template {
articles: &articles,
},
}.to_string()))
}))
Box::new(data.join(head).and_then(move |(articles, head)| {
Ok(head.with_body(
system_page(
None, // Hmm, should perhaps accept `base` as argument
"Sitemap",
Template {
articles: &articles,
},
)
.to_string(),
))
}))
}
}

View file

@ -1,9 +1,9 @@
use futures::{self, Future};
use hyper;
use hyper::header::Location;
use hyper::server::*;
use web::{Resource, ResponseFuture};
use crate::web::{Resource, ResponseFuture};
pub struct TemporaryRedirectResource {
location: String,
@ -14,14 +14,17 @@ impl TemporaryRedirectResource {
Self { location }
}
pub fn from_slug<S: AsRef<str>>(slug: S) -> Self {
pub fn from_slug<S: AsRef<str>>(slug: S, edit: bool) -> Self {
let base = if slug.as_ref().is_empty() {
"."
} else {
slug.as_ref()
};
let tail = if edit { "?edit" } else { "" };
Self {
location:
if slug.as_ref().is_empty() {
"."
} else {
slug.as_ref()
}.to_owned()
location: format!("{}{}", base, tail),
}
}
}
@ -33,18 +36,18 @@ impl Resource for TemporaryRedirectResource {
}
fn head(&self) -> ResponseFuture {
Box::new(futures::finished(Response::new()
.with_status(hyper::StatusCode::TemporaryRedirect)
.with_header(Location::new(self.location.clone()))
Box::new(futures::finished(
Response::new()
.with_status(hyper::StatusCode::TemporaryRedirect)
.with_header(Location::new(self.location.clone())),
))
}
fn get(self: Box<Self>) -> ResponseFuture {
Box::new(self.head()
.and_then(move |head| {
Ok(head
.with_body(format!("Moved to {}", self.location)))
}))
Box::new(
self.head()
.and_then(move |head| Ok(head.with_body(format!("Moved to {}", self.location)))),
)
}
fn put(self: Box<Self>, _body: hyper::Body, _identity: Option<String>) -> ResponseFuture {

View file

@ -4,20 +4,20 @@
use std::fmt;
use futures::{self, Future};
use hyper::header::{Accept, ContentType, Server};
use hyper::mime;
use hyper::server::*;
use hyper;
use assets::{StyleCss, SearchJs};
use build_config;
use web::Lookup;
use wiki_lookup::WikiLookup;
use crate::assets::{SearchJs, StyleCss, ThemesCss};
use crate::build_config;
use crate::theme;
use crate::web::Lookup;
use crate::wiki_lookup::WikiLookup;
lazy_static! {
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] }
@ -27,15 +27,53 @@ header! { (XIdentity, "X-Identity") => [String] }
pub struct Layout<'a, T: 'a + fmt::Display> {
pub base: Option<&'a str>,
pub title: &'a str,
pub body: &'a T,
pub theme: theme::Theme,
pub body: T,
}
impl<'a, T: 'a + fmt::Display> Layout<'a, T> {
pub fn style_css_checksum(&self) -> &str { StyleCss::checksum() }
pub fn search_js_checksum(&self) -> &str { SearchJs::checksum() }
pub fn themes_css(&self) -> &str {
ThemesCss::resource_name()
}
pub fn style_css(&self) -> &str {
StyleCss::resource_name()
}
pub fn search_js(&self) -> &str {
SearchJs::resource_name()
}
pub fn project_name(&self) -> &str { build_config::PROJECT_NAME }
pub fn version(&self) -> &str { build_config::VERSION.as_str() }
pub fn project_name(&self) -> &str {
build_config::PROJECT_NAME
}
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)]
@ -53,41 +91,39 @@ pub struct Site {
impl Site {
pub fn new(root: WikiLookup, trust_identity: bool) -> Site {
Site { root, trust_identity }
Site {
root,
trust_identity,
}
}
fn not_found(base: Option<&str>) -> Response {
Response::new()
.with_header(ContentType(TEXT_HTML.clone()))
.with_body(Layout {
base: base,
title: "Not found",
body: &NotFound,
}.to_string())
.with_body(system_page(base, "Not found", NotFound).to_string())
.with_status(hyper::StatusCode::NotFound)
}
fn internal_server_error(base: Option<&str>, err: Box<::std::error::Error + Send + Sync>) -> Response {
fn internal_server_error(
base: Option<&str>,
err: Box<dyn ::std::error::Error + Send + Sync>,
) -> Response {
eprintln!("Internal Server Error:\n{:#?}", err);
Response::new()
.with_header(ContentType(TEXT_HTML.clone()))
.with_body(Layout {
base,
title: "Internal server error",
body: &InternalServerError,
}.to_string())
.with_body(system_page(base, "Internal server error", InternalServerError).to_string())
.with_status(hyper::StatusCode::InternalServerError)
}
}
fn root_base_from_request_uri(path: &str) -> Option<String> {
assert!(path.starts_with("/"));
assert!(path.starts_with('/'));
let slashes = path[1..].matches('/').count();
match slashes {
0 => None,
n => Some(::std::iter::repeat("../").take(n).collect())
n => Some("../".repeat(n)),
}
}
@ -95,7 +131,7 @@ impl Service for Site {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<futures::Future<Item = Response, Error = Self::Error>>;
type Future = Box<dyn futures::Future<Item = Response, Error = Self::Error>>;
fn call(&self, req: Request) -> Self::Future {
let (method, uri, _http_version, headers, body) = req.deconstruct();
@ -107,29 +143,31 @@ impl Service for Site {
false => None,
};
let accept_header = headers.get().map(|x: &Accept| x.clone()).unwrap_or(Accept(vec![]));
let accept_header = headers.get().cloned().unwrap_or_else(|| Accept(vec![]));
let base = root_base_from_request_uri(uri.path());
let base2 = base.clone(); // Bah, stupid clone
Box::new(self.root.lookup(uri.path(), uri.query())
.and_then(move |resource| match resource {
Some(mut resource) => {
use hyper::Method::*;
resource.hacky_inject_accept_header(accept_header);
match method {
Options => Box::new(futures::finished(resource.options())),
Head => resource.head(),
Get => resource.get(),
Put => resource.put(body, identity),
Post => resource.post(body, identity),
_ => Box::new(futures::finished(resource.method_not_allowed()))
Box::new(
self.root
.lookup(uri.path(), uri.query())
.and_then(move |resource| match resource {
Some(mut resource) => {
use hyper::Method::*;
resource.hacky_inject_accept_header(accept_header);
match method {
Options => Box::new(futures::finished(resource.options())),
Head => resource.head(),
Get => resource.get(),
Put => resource.put(body, identity),
Post => resource.post(body, identity),
_ => Box::new(futures::finished(resource.method_not_allowed())),
}
}
},
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_ref().map(|x| &**x), err)))
.map(|response| response.with_header(SERVER.clone()))
None => Box::new(futures::finished(Self::not_found(base.as_deref()))),
})
.or_else(move |err| Ok(Self::internal_server_error(base2.as_deref(), err)))
.map(|response| response.with_header(SERVER.clone())),
)
}
}

File diff suppressed because it is too large Load diff

232
src/theme.rs Normal file
View file

@ -0,0 +1,232 @@
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(())
}
}

View file

@ -1,9 +1,7 @@
use futures;
pub trait Lookup {
type Resource;
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;
}

View file

@ -1,5 +1,5 @@
mod resource;
mod lookup;
mod resource;
pub use self::resource::*;
pub use self::lookup::*;
pub use self::resource::*;

View file

@ -1,15 +1,13 @@
use futures;
use futures::{Future, Stream};
use hyper::{self, header, mime, server};
use hyper::server::Response;
use std;
use hyper::{self, header, mime, server};
lazy_static! {
static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap();
}
pub type Error = Box<std::error::Error + Send + Sync>;
pub type ResponseFuture = Box<futures::Future<Item = server::Response, Error = Error>>;
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type ResponseFuture = Box<dyn futures::Future<Item = server::Response, Error = Error>>;
pub trait Resource {
fn allow(&self) -> Vec<hyper::Method>;
@ -23,22 +21,24 @@ pub trait Resource {
}
fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
where Self: 'static
where
Self: 'static,
{
Box::new(body
.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed()))
Box::new(
body.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed())),
)
}
fn post(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
where Self: 'static
where
Self: 'static,
{
Box::new(body
.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed()))
Box::new(
body.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed())),
)
}

View file

@ -2,48 +2,36 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::str::Utf8Error;
use futures::{Future, finished, failed, done};
use futures::future::FutureResult;
use futures::{done, failed, finished, Future};
use percent_encoding::percent_decode;
use slug::slugify;
use resources::*;
use assets::*;
use state::State;
use web::{Lookup, Resource};
use crate::resources::*;
use crate::state::State;
use crate::web::{Lookup, Resource};
type BoxResource = Box<Resource + Sync + Send>;
type ResourceFn = Box<Fn() -> BoxResource + Sync + Send>;
#[allow(unused)]
use crate::assets::*;
type BoxResource = Box<dyn Resource + Sync + Send>;
type ResourceFn = Box<dyn Fn() -> BoxResource + Sync + Send>;
lazy_static! {
static ref ASSETS_MAP: HashMap<String, ResourceFn> = hashmap!{
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(
static ref LICENSES_MAP: HashMap<&'static str, ResourceFn> = hashmap! {
"bsd-3-clause" => Box::new(|| Box::new(
HtmlResource::new(Some("../"), "The 3-Clause BSD License", include_str!("licenses/bsd-3-clause.html"))
) as BoxResource) as ResourceFn,
"gpl3".to_owned() => Box::new(|| Box::new(
"gpl3" => Box::new(|| Box::new(
HtmlResource::new(Some("../"), "GNU General Public License", include_str!("licenses/gpl3.html"))
) as BoxResource) as ResourceFn,
"mit".to_owned() => Box::new(|| Box::new(
"mit" => Box::new(|| Box::new(
HtmlResource::new(Some("../"), "The MIT License", include_str!("licenses/mit.html"))
) as BoxResource) as ResourceFn,
"mpl2".to_owned() => Box::new(|| Box::new(
"mpl2" => Box::new(|| Box::new(
HtmlResource::new(Some("../"), "Mozilla Public License Version 2.0", include_str!("licenses/mpl2.html"))
) as BoxResource) as ResourceFn,
"sil-ofl-1.1".to_owned() => Box::new(|| Box::new(
"sil-ofl-1.1" => Box::new(|| Box::new(
HtmlResource::new(Some("../"), "SIL Open Font License", include_str!("licenses/sil-ofl-1.1.html"))
) as BoxResource) as ResourceFn,
};
@ -66,9 +54,10 @@ fn split_one(path: &str) -> Result<(Cow<str>, Option<&str>), Utf8Error> {
Ok((head, tail))
}
fn map_lookup(map: &HashMap<String, ResourceFn>, path: &str) ->
FutureResult<Option<BoxResource>, Box<::std::error::Error + Send + Sync>>
{
fn map_lookup(
map: &HashMap<&str, ResourceFn>,
path: &str,
) -> FutureResult<Option<BoxResource>, Box<dyn ::std::error::Error + Send + Sync>> {
let (head, tail) = match split_one(path) {
Ok(x) => x,
Err(x) => return failed(x.into()),
@ -84,13 +73,49 @@ fn map_lookup(map: &HashMap<String, ResourceFn>, path: &str) ->
}
}
#[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 {
pub fn new(state: State, show_authors: bool) -> WikiLookup {
let changes_lookup = ChangesLookup::new(state.clone(), show_authors);
let diff_lookup = DiffLookup::new(state.clone());
let search_lookup = SearchLookup::new(state.clone());
WikiLookup { state, changes_lookup, diff_lookup, search_lookup }
WikiLookup {
state,
changes_lookup,
diff_lookup,
search_lookup,
}
}
fn revisions_lookup(&self, path: &str, _query: Option<&str>) -> <Self as Lookup>::Future {
@ -108,12 +133,12 @@ impl WikiLookup {
};
Box::new(
self.state.get_article_revision(article_id, revision)
.and_then(|article_revision|
Ok(article_revision.map(move |x| Box::new(
ArticleRevisionResource::new(x)
) as BoxResource))
)
self.state
.get_article_revision(article_id, revision)
.and_then(|article_revision| {
Ok(article_revision
.map(move |x| Box::new(ArticleRevisionResource::new(x)) as BoxResource))
}),
)
}
@ -130,14 +155,11 @@ impl WikiLookup {
Err(_) => return Box::new(finished(None)),
};
Box::new(
self.state.get_article_slug(article_id)
.and_then(|slug|
Ok(slug.map(|slug| Box::new(
TemporaryRedirectResource::new(format!("../{}", slug))
) as BoxResource))
)
)
Box::new(self.state.get_article_slug(article_id).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 {
@ -163,26 +185,30 @@ impl WikiLookup {
};
match (head.as_ref(), tail) {
("_about", None) =>
Box::new(finished(Some(Box::new(AboutResource::new()) as BoxResource))),
("_about", Some(license)) =>
Box::new(map_lookup(&LICENSES_MAP, license)),
("_assets", Some(asset)) =>
Box::new(map_lookup(&ASSETS_MAP, asset)),
("_by_id", Some(tail)) =>
self.by_id_lookup(tail, query),
("_changes", None) =>
Box::new(self.changes_lookup.lookup(query)),
("_diff", Some(tail)) =>
self.diff_lookup_f(tail, query),
("_new", None) =>
Box::new(finished(Some(Box::new(NewArticleResource::new(self.state.clone(), None)) as BoxResource))),
("_revisions", Some(tail)) =>
self.revisions_lookup(tail, query),
("_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))),
("_about", None) => Box::new(finished(Some(
Box::new(AboutResource::new()) as BoxResource
))),
("_about", Some(license)) => Box::new(map_lookup(&LICENSES_MAP, license)),
#[cfg(feature = "dynamic-assets")]
("_assets", Some(asset)) => Box::new(fs_lookup(
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/"),
asset,
)),
#[cfg(not(feature = "dynamic-assets"))]
("_assets", Some(asset)) => Box::new(map_lookup(&ASSETS_MAP, asset)),
("_by_id", Some(tail)) => self.by_id_lookup(tail, query),
("_changes", None) => Box::new(self.changes_lookup.lookup(query)),
("_diff", Some(tail)) => self.diff_lookup_f(tail, query),
("_new", None) => Box::new(finished(Some(Box::new(NewArticleResource::new(
self.state.clone(),
None,
true,
)) as BoxResource))),
("_revisions", Some(tail)) => self.revisions_lookup(tail, query),
("_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)),
}
}
@ -198,42 +224,49 @@ impl WikiLookup {
return Box::new(finished(None));
}
let edit = query == Some("edit");
// Normalize all user-generated slugs:
let slugified_slug = slugify(&slug);
if slugified_slug != slug {
return Box::new(finished(Some(
Box::new(TemporaryRedirectResource::from_slug(slugified_slug)) as BoxResource
Box::new(TemporaryRedirectResource::from_slug(slugified_slug, edit)) as BoxResource,
)));
}
let state = self.state.clone();
let edit = query == Some("edit");
let slug = slug.into_owned();
use state::SlugLookup;
Box::new(self.state.lookup_slug(slug.clone())
.and_then(move |x| Ok(Some(match x {
SlugLookup::Miss =>
Box::new(NewArticleResource::new(state, Some(slug))) as BoxResource,
SlugLookup::Hit { article_id, revision } =>
Box::new(ArticleResource::new(state, article_id, revision, edit)) as BoxResource,
SlugLookup::Redirect(slug) =>
Box::new(TemporaryRedirectResource::from_slug(slug)) as BoxResource,
})))
)
use crate::state::SlugLookup;
Box::new(self.state.lookup_slug(slug.clone()).and_then(move |x| {
Ok(Some(match x {
SlugLookup::Miss => {
Box::new(NewArticleResource::new(state, Some(slug), edit)) as BoxResource
}
SlugLookup::Hit {
article_id,
revision,
} => {
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 {
type Resource = BoxResource;
type Error = Box<::std::error::Error + Send + Sync>;
type Future = Box<Future<Item = Option<Self::Resource>, Error = Self::Error>>;
type Error = Box<dyn ::std::error::Error + Send + Sync>;
type Future = Box<dyn Future<Item = Option<Self::Resource>, Error = Self::Error>>;
fn lookup(&self, path: &str, query: Option<&str>) -> Self::Future {
assert!(path.starts_with("/"));
assert!(path.starts_with('/'));
let path = &path[1..];
if path.starts_with("_") {
if path.starts_with('_') {
self.reserved_lookup(path, query)
} else {
self.article_lookup(path, query)

View file

@ -1,9 +1,3 @@
<div class="container">
<header>
<h1>About Sausagewiki</h1>
</header>
<article>
<p>This site is running Sausagewiki, a simple, self-contained wiki engine,
version {{version()}}.</p>
<p>Copyright &copy; 2017 Magnus Hovland Hoff.</p>
@ -20,9 +14,9 @@ See also <a href="_about/gpl3">the full license text</a> and the
</p>
<p>
A huge thanks to <a href="https://www.rust-lang.org/en-US/">Rust</a> and
<a href="https://www.sqlite.org/">SQLite</a>. Without these amazing projects,
Sausagewiki would never have materialized. Another big thanks for the support,
Without <a href="https://www.rust-lang.org/en-US/">Rust</a> and
<a href="https://www.sqlite.org/">SQLite</a>, Sausagewiki would never have
materialized. Huge thanks to the creators. Another big thanks for the support,
discussions and testing by the amazing developers at Revolverhuset.
</p>
@ -39,7 +33,3 @@ copyright holders and distributed under various licenses:
{{/deps}}
</tbody>
</table>
</article>
</div>
{{>footer/default.html}}

View file

@ -1,16 +1,24 @@
<script src="_assets/script-{{script_js_checksum()}}.js" defer></script>
<script src="_assets/{{script_js()}}" defer></script>
<div class="container {{#edit?}}edit{{/edit}}">
<div class="rendered">
{{>article_contents.html}}
</div>
<div class="editor">
<form action="" method="POST">
<form autocomplete="off" id="article-editor" action="" method="POST">
<div class="editor">
<div class="hero">
<header>
<h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title" required></h1>
</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>
<p>
@ -20,15 +28,26 @@
</p>
</article>
<div class="editor-controls">
{{#cancel_url}}
<a class="cancel" href="{{.}}">Cancel</a>
{{/cancel_url}}
<button type=submit>Save</button>
</div>
</form>
<div class="editor-controls">
{{#edit?}}
<div class="cancel-interaction-group {{#cancel_url}}interaction-group--root--enabled{{/cancel_url}}{{^cancel_url}}interaction-group--root--disabled{{/cancel_url}}">
<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 {{^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>
</form>
</div>
<footer>

View file

@ -1,6 +1,8 @@
<div class="hero">
<header>
<h1>{{title}}</h1>
</header>
</div>
<article>
{{{rendered}}}

View file

@ -1,5 +1,3 @@
<div class="container">
<div class="notice">
<p>
You are viewing an historical version of <a href="{{link_current}}">this article</a>,
@ -11,11 +9,4 @@
</p>
</div>
<div class="rendered">
{{>article_contents.html}}
</div>
</div>
<footer>
{{>footer/items.html}}
</footer>
{{{rendered}}}

View file

@ -1,9 +1,3 @@
<div class="container">
<header>
<h1>Changes</h1>
</header>
<article>
<p>
These are the {{^newer}}most recent{{/newer}} changes
made to{{{subject_clause()}}}{{#author()}} by {{.}}{{/author()}}.
@ -44,7 +38,3 @@
><li><a rel="last" href="{{.end}}">First changes</a></li
></ul></nav>{{/older}}
{{#changes?}}{{^older}}<p>There are no older changes.</p>{{/older}}{{/changes}}
</article>
</div>
{{>footer/default.html}}

View file

@ -1,9 +1,16 @@
<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">
<p>
You are viewing the difference between two {{#consecutive?}}consecutive{{/consecutive}}
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>
@ -12,10 +19,6 @@
</p>
</div>
<header>
<h1>{{#title}}{{#.removed}}<span class="removed">{{.}}</span>{{/.removed}}{{#.same}}{{.}}{{/.same}}{{#.added}}<span class="added">{{.}}</span>{{/.added}}{{/title}}</h1>
</header>
<article>
<pre class="diff">{{#lines}}{{#.removed}}<span class="removed">{{.}}
</span>{{/.removed}}{{#.same}}<span class="same">{{.}}

View file

@ -1,11 +1 @@
<div class="container">
<header>
<h1>Not found</h1>
</header>
<article>
<p>This page was not found.</p>
</article>
</div>
{{>../footer/default.html}}
<p>This page was not found.</p>

View file

@ -1,11 +1 @@
<div class="container">
<header>
<h1>Internal server error</h1>
</header>
<article>
<p>An error has occurred.</p>
</article>
</div>
{{>../footer/default.html}}
<p>An error has occurred.</p>

View file

@ -3,12 +3,13 @@
<head>
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
{{#base}}<base href="{{.}}">{{/base}}
<link rel=preload href="_assets/amatic-sc-v9-latin-regular.woff" as=font type="font/woff" crossorigin=anonymous>
<link href="_assets/style-{{style_css_checksum()}}.css" rel="stylesheet">
<link href="_assets/{{themes_css()}}" rel="stylesheet">
<link href="_assets/{{style_css()}}" rel="stylesheet">
<meta name="generator" content="{{project_name()}} {{version()}}" />
</head>
<body>
<body class="{{theme.css_class()}}">
{{>search_input.html}}
{{{body}}}
</body>

View file

@ -1,9 +1,3 @@
<div class="container">
<header>
<h1>Search</h1>
</header>
<article>
{{#hits?}}
<p>Search results for the query <b>{{query}}</b>:</p>
@ -25,8 +19,3 @@
{{^hits?}}
<p>Your search for <b>{{query}}</b> gave no results.</p>
{{/hits}}
</article>
</div>
{{>footer/default.html}}

View file

@ -1,7 +1,11 @@
<div class="search-container">
<form class="search keyboard-focus-control" action=_search method=GET>
<input data-focusindex="0" type=search name=q placeholder=search autocomplete=off>
<ul class="live-results search-results">
</ul>
<div class="search-widget-container">
<input data-focusindex="0" type=search name=q placeholder=Search autocomplete=off>
<ul class="live-results search-results">
</ul>
</div>
</form>
<ul id="search-result-prototype" class="prototype"><li class="search-result"><a tabindex="0" class="link" href=""><span class="title"></span> &ndash; <span class="snippet"></span></a></li></ul>
<script src="_assets/search-{{search_js_checksum()}}.js" defer></script>
<script src="_assets/{{search_js()}}" defer></script>
</div>

View file

@ -1,15 +1,5 @@
<div class="container">
<header>
<h1>Sitemap</h1>
</header>
<article>
<ul class="dense"
{{#articles}}
><li><a href="{{.link()}}">{{.title}}</a></li
{{/articles}}
></ul>
</article>
</div>
{{>footer/default.html}}

View file

@ -1,7 +1,9 @@
<div class="container">
<div class="hero">
<header>
<h1>{{title}}</h1>
</header>
</div>
<article>
{{{html_body}}}

74
themes/generate.py Executable file
View file

@ -0,0 +1,74 @@
#!/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) + "]")

369
themes/material-colors.json Normal file
View file

@ -0,0 +1,369 @@
{
"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"
]
}
]
}