diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..7a8f771e
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "_assets/js/vendor/ace"]
+	path = _assets/js/vendor/ace
+	url = https://github.com/ajaxorg/ace-builds
diff --git a/_assets/css/fonts.css b/_assets/css/fonts.css
new file mode 100644
index 00000000..1911d377
--- /dev/null
+++ b/_assets/css/fonts.css
@@ -0,0 +1,137 @@
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-cyrillic-ext.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-cyrillic.woff2) format('woff2');
+  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-greek-ext.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-greek.woff2) format('woff2');
+  unicode-range: U+0370-03FF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-vietnamese.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-latin-ext.woff2) format('woff2');
+  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Roboto'), local('Roboto-Regular'), url(roboto/normal-latin.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-cyrillic-ext.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-cyrillic.woff2) format('woff2');
+  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-greek-ext.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-greek.woff2) format('woff2');
+  unicode-range: U+0370-03FF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-vietnamese.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-latin-ext.woff2) format('woff2');
+  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+  font-family: 'Roboto';
+  font-style: normal;
+  font-weight: 500;
+  src: local('Roboto Medium'), local('Roboto-Medium'), url(roboto/medium-latin.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+@font-face {
+  font-family: 'Material Icons';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Material Icons'), local('MaterialIcons-Regular'), url(material/icons.woff2) format('woff2');
+}
+
+.prompt .file-list ul li:before,
+.material-icons {
+  font-family: 'Material Icons';
+  font-weight: normal;
+  font-style: normal;
+  font-size: 24px;
+  line-height: 1;
+  letter-spacing: normal;
+  text-transform: none;
+  display: inline-block;
+  white-space: nowrap;
+  word-wrap: normal;
+  direction: ltr;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  -moz-osx-font-smoothing: grayscale;
+  font-feature-settings: 'liga';
+}
diff --git a/_assets/css/material/icons.woff2 b/_assets/css/material/icons.woff2
new file mode 100644
index 00000000..9fa21125
Binary files /dev/null and b/_assets/css/material/icons.woff2 differ
diff --git a/_assets/css/normalize.css b/_assets/css/normalize.css
new file mode 100644
index 00000000..9b77e0eb
--- /dev/null
+++ b/_assets/css/normalize.css
@@ -0,0 +1,461 @@
+/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Change the default font family in all browsers (opinionated).
+ * 2. Correct the line height in all browsers.
+ * 3. Prevent adjustments of font size after orientation changes in
+ *    IE on Windows Phone and in iOS.
+ */
+
+/* Document
+   ========================================================================== */
+
+html {
+  font-family: sans-serif; /* 1 */
+  line-height: 1.15; /* 2 */
+  -ms-text-size-adjust: 100%; /* 3 */
+  -webkit-text-size-adjust: 100%; /* 3 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers (opinionated).
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+article,
+aside,
+footer,
+header,
+nav,
+section {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in IE.
+ */
+
+figcaption,
+figure,
+main { /* 1 */
+  display: block;
+}
+
+/**
+ * Add the correct margin in IE 8.
+ */
+
+figure {
+  margin: 1em 40px;
+}
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * 1. Remove the gray background on active links in IE 10.
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
+ */
+
+a {
+  background-color: transparent; /* 1 */
+  -webkit-text-decoration-skip: objects; /* 2 */
+}
+
+/**
+ * Remove the outline on focused links when they are also active or hovered
+ * in all browsers (opinionated).
+ */
+
+a:active,
+a:hover {
+  outline-width: 0;
+}
+
+/**
+ * 1. Remove the bottom border in Firefox 39-.
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
+ */
+
+b,
+strong {
+  font-weight: inherit;
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font style in Android 4.3-.
+ */
+
+dfn {
+  font-style: italic;
+}
+
+/**
+ * Add the correct background and color in IE 9-.
+ */
+
+mark {
+  background-color: #ff0;
+  color: #000;
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+audio,
+video {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in iOS 4-7.
+ */
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+/**
+ * Remove the border on images inside links in IE 10-.
+ */
+
+img {
+  border-style: none;
+}
+
+/**
+ * Hide the overflow in IE.
+ */
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers (opinionated).
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: sans-serif; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+ *    controls in Android 4.
+ * 2. Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+html [type="button"], /* 1 */
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button; /* 2 */
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Change the border, margin, and padding in all browsers (opinionated).
+ */
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * 1. Add the correct display in IE 9-.
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  display: inline-block; /* 1 */
+  vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Remove the default vertical scrollbar in IE.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10-.
+ * 2. Remove the padding in IE 10-.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in Edge, IE, and Firefox.
+ */
+
+details, /* 1 */
+menu {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Scripting
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+canvas {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in IE.
+ */
+
+template {
+  display: none;
+}
+
+/* Hidden
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10-.
+ */
+
+[hidden] {
+  display: none;
+}
diff --git a/_assets/css/roboto/medium-cyrillic-ext.woff2 b/_assets/css/roboto/medium-cyrillic-ext.woff2
new file mode 100644
index 00000000..f63bc9a1
Binary files /dev/null and b/_assets/css/roboto/medium-cyrillic-ext.woff2 differ
diff --git a/_assets/css/roboto/medium-cyrillic.woff2 b/_assets/css/roboto/medium-cyrillic.woff2
new file mode 100644
index 00000000..b3ca824d
Binary files /dev/null and b/_assets/css/roboto/medium-cyrillic.woff2 differ
diff --git a/_assets/css/roboto/medium-greek-ext.woff2 b/_assets/css/roboto/medium-greek-ext.woff2
new file mode 100644
index 00000000..7e1a8078
Binary files /dev/null and b/_assets/css/roboto/medium-greek-ext.woff2 differ
diff --git a/_assets/css/roboto/medium-greek.woff2 b/_assets/css/roboto/medium-greek.woff2
new file mode 100644
index 00000000..314cf3f8
Binary files /dev/null and b/_assets/css/roboto/medium-greek.woff2 differ
diff --git a/_assets/css/roboto/medium-latin-ext.woff2 b/_assets/css/roboto/medium-latin-ext.woff2
new file mode 100644
index 00000000..604b8935
Binary files /dev/null and b/_assets/css/roboto/medium-latin-ext.woff2 differ
diff --git a/_assets/css/roboto/medium-latin.woff2 b/_assets/css/roboto/medium-latin.woff2
new file mode 100644
index 00000000..5f96609d
Binary files /dev/null and b/_assets/css/roboto/medium-latin.woff2 differ
diff --git a/_assets/css/roboto/medium-vietnamese.woff2 b/_assets/css/roboto/medium-vietnamese.woff2
new file mode 100644
index 00000000..d92b7125
Binary files /dev/null and b/_assets/css/roboto/medium-vietnamese.woff2 differ
diff --git a/_assets/css/roboto/normal-cyrillic-ext.woff2 b/_assets/css/roboto/normal-cyrillic-ext.woff2
new file mode 100644
index 00000000..e4546e49
Binary files /dev/null and b/_assets/css/roboto/normal-cyrillic-ext.woff2 differ
diff --git a/_assets/css/roboto/normal-cyrillic.woff2 b/_assets/css/roboto/normal-cyrillic.woff2
new file mode 100644
index 00000000..d08397f7
Binary files /dev/null and b/_assets/css/roboto/normal-cyrillic.woff2 differ
diff --git a/_assets/css/roboto/normal-greek-ext.woff2 b/_assets/css/roboto/normal-greek-ext.woff2
new file mode 100644
index 00000000..ed0b13ca
Binary files /dev/null and b/_assets/css/roboto/normal-greek-ext.woff2 differ
diff --git a/_assets/css/roboto/normal-greek.woff2 b/_assets/css/roboto/normal-greek.woff2
new file mode 100644
index 00000000..f630772d
Binary files /dev/null and b/_assets/css/roboto/normal-greek.woff2 differ
diff --git a/_assets/css/roboto/normal-latin-ext.woff2 b/_assets/css/roboto/normal-latin-ext.woff2
new file mode 100644
index 00000000..0c7aec28
Binary files /dev/null and b/_assets/css/roboto/normal-latin-ext.woff2 differ
diff --git a/_assets/css/roboto/normal-latin.woff2 b/_assets/css/roboto/normal-latin.woff2
new file mode 100644
index 00000000..120796bb
Binary files /dev/null and b/_assets/css/roboto/normal-latin.woff2 differ
diff --git a/_assets/css/roboto/normal-vietnamese.woff2 b/_assets/css/roboto/normal-vietnamese.woff2
new file mode 100644
index 00000000..7936b665
Binary files /dev/null and b/_assets/css/roboto/normal-vietnamese.woff2 differ
diff --git a/_assets/css/styles.css b/_assets/css/styles.css
new file mode 100644
index 00000000..eb9d8a5b
--- /dev/null
+++ b/_assets/css/styles.css
@@ -0,0 +1,1207 @@
+body {
+  font-family: 'Roboto', sans-serif;
+  padding-top: 7.8em;
+  background-color: #f8f8f8;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+*,
+*:hover,
+*:active,
+*:focus {
+  outline: 0
+}
+
+a {
+  text-decoration: none;
+}
+
+img {
+  max-width: 100%;
+}
+
+audio,
+video {
+  width: 100%;
+}
+
+pre {
+  padding: 1em;
+  border: 1px solid #e6e6e6;
+  border-radius: 0.5em;
+  background-color: #f5f5f5;
+  white-space: pre-wrap;
+  white-space: -moz-pre-wrap;
+  white-space: -pre-wrap;
+  white-space: -o-pre-wrap;
+  word-wrap: break-word;
+}
+
+button {
+  border: 0;
+  padding: .5em 1em;
+  margin-left: .5em;
+  border-radius: .1em;
+  cursor: pointer;
+  background: #2196f3;
+  color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.05);
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
+  transition: .1s ease all;
+}
+
+button:hover {
+  background-color: #1E88E5;
+}
+
+.mobile-only {
+  display: none !important;
+}
+
+.container {
+  width: 95%;
+  max-width: 960px;
+  margin: 1em auto 0;
+}
+
+i.spin {
+  animation: 1s spin linear infinite;
+}
+
+.pdf {
+  width: 100%;
+  height: calc(100vh - 13em);
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            EDITOR           *
+ * * * * * * * * * * * * * * * */
+
+#editor .source {
+  display: none;
+}
+
+#editor .content {
+  background: #fff;
+  padding: 1em 0;
+}
+
+#editor #ace,
+#editor h2,
+#editor .frontmatter {
+  width: 95%;
+  max-width: 960px;
+  margin: 1em auto 0;
+}
+
+#editor h2 {
+  margin: 1.5em auto 1em;
+  color: rgba(0, 0, 0, 0.3);
+  font-weight: 500;
+}
+
+#editor .ace_gutter {
+  background-color: #fff;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *       EDITOR - MARKDOWN     *
+ * * * * * * * * * * * * * * * */
+
+.frontmatter {
+  column-count: 3;
+  column-gap: 1em;
+  column-fill: balance;
+  /* display: flex; */
+  /* flex-wrap: wrap; */
+  /* justify-content: space-between; */
+  /* flex-grow: 1; */
+}
+
+.frontmatter label {
+  display: block;
+  width: calc(100% - 1em);
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.frontmatter label,
+.frontmatter h3 {
+  font-weight: 500;
+  margin: 0 0;
+  color: rgba(0, 0, 0, 0.6);
+}
+
+.frontmatter input,
+.frontmatter textarea {
+  display: block;
+  width: 100%;
+  border: 0;
+  margin-top: .5em;
+  padding: 0;
+  line-height: 1;
+}
+
+.frontmatter .block,
+.frontmatter fieldset[data-type="array"],
+.button {
+  position: relative;
+  background: #fff;
+  border-radius: .2em;
+  border: 1px solid rgba(0, 0, 0, 0.075);
+  padding: .5em;
+  break-inside: avoid;
+  margin: 0 0 1em;
+  width: 100%;
+  display: inline-block;
+}
+
+.frontmatter fieldset[data-type="object"] {
+  position: relative;
+  margin: 0;
+}
+
+.frontmatter .button {
+  background-color: #2196f3;
+  color: #fff;
+  cursor: pointer;
+  text-align: center;
+}
+
+[data-type="array-item"] {
+  position: relative;
+}
+
+[data-type="array-item"] .action {
+  top: 0;
+  right: 0;
+}
+
+.frontmatter textarea {
+  resize: none;
+}
+
+[data-type="array-item"] input {
+  width: calc(100% - 1em);
+}
+
+.block .action,
+fieldset .action {
+  position: absolute;
+  top: .5em;
+  right: .5em;
+}
+
+.block>.action,
+fieldset>.action {
+  opacity: 0;
+}
+
+.block:hover>.action,
+fieldset:hover>.action {
+  opacity: 1;
+}
+
+.block .action.add,
+fieldset .action.add {
+  right: 1.5em;
+}
+
+.frontmatter .action i {
+  padding: 0;
+  font-size: 1em;
+}
+
+fieldset {
+  border: 0;
+  padding: 0;
+}
+
+.frontmatter>fieldset h3,
+.frontmatter>.group h3 {
+  font-size: 1.5em;
+  margin-bottom: .5em;
+}
+
+fieldset h3,
+.group h3 {
+  font-size: 0.9em;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            ACTION           *
+ * * * * * * * * * * * * * * * */
+
+.action {
+  display: inline-block;
+  cursor: pointer;
+  -webkit-transition: 0.2s ease all;
+  transition: 0.2s ease all;
+  border: 0;
+  margin: 0;
+  color: #546E7A;
+  border-radius: 50%;
+}
+
+.action.disabled {
+  opacity: 0.2;
+  cursor: not-allowed;
+}
+
+.action i {
+  padding: 0.4em;
+  -webkit-transition: 0.2s ease-in-out all;
+  transition: 0.2s ease-in-out all;
+  border-radius: 50%;
+}
+
+.action:hover i {
+  background-color: rgba(0, 0, 0, .1);
+}
+
+.action ul {
+  position: absolute;
+  top: 0;
+  color: #7d7d7d;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  flex-direction: column;
+  display: flex;
+}
+
+.action ul li {
+  line-height: 1;
+  padding: .7em;
+  transition: .1s ease background-color;
+}
+
+.action ul li:hover {
+  background-color: rgba(0, 0, 0, 0.04);
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *         NEW FILE/DIR        *
+ * * * * * * * * * * * * * * * */
+
+.floating {
+  position: fixed;
+  bottom: 1em;
+  right: 1em;
+}
+
+.floating .action {
+  background-color: #2196f3 !important;
+  color: #fff;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
+}
+
+#newdir {
+  position: fixed;
+  bottom: 1.3em;
+  right: 5em;
+  transition: .2s ease all;
+  opacity: 0;
+  border: 0;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
+  padding: .5em;
+  width: 22em;
+  border-radius: .2em;
+}
+
+#newdir.enabled {
+  opacity: 1;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            HEADER           *
+ * * * * * * * * * * * * * * * */
+
+header {
+  z-index: 1000;
+  background-color: #fff;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.075);
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  padding: 0;
+}
+
+header a,
+header a:hover {
+  color: inherit;
+}
+
+header p i {
+  font-size: 1em !important;
+  color: rgba(255, 255, 255, .31);
+}
+
+header>div {
+  display: flex;
+  width: 100%;
+  padding: 0.5em 0.5em 0.5em 1em;
+  align-items: center;
+}
+
+header p {
+  display: inline-block;
+  margin: 0;
+  vertical-align: middle;
+}
+
+header p a,
+header p a:hover {
+  color: inherit;
+}
+
+header .action span {
+  display: none;
+}
+
+header>div div {
+  vertical-align: middle;
+  position: relative;
+}
+
+#logout {
+  border-radius: 0;
+  margin-left: auto;
+  padding: .15em;
+}
+
+#click-overlay {
+  display: none;
+  position: fixed;
+  cursor: pointer;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+}
+
+#click-overlay.active {
+  display: block;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            TOP BAR          *
+ * * * * * * * * * * * * * * * */
+
+#top-bar {
+  height: 4em;
+}
+
+#top-bar>div:nth-child(1) {
+  margin-right: 1em;
+  font-weight: 500;
+  font-size: 1.5em;
+  line-height: 2;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *          SEARCH BAR         *
+ * * * * * * * * * * * * * * * */
+
+#search {
+  position: relative;
+  display: flex;
+  height: 100%;
+  padding: 0.75em;
+  vertical-align: middle;
+  border-radius: 0.3em;
+  background-color: #f5f5f5;
+  transition: .1s ease all;
+  width: 100%;
+  max-width: 25em;
+}
+
+#search.active {
+  background-color: #fff;
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
+}
+
+#search.active i,
+#search.active input {
+  color: #212121;
+}
+
+#search i,
+#search input {
+  vertical-align: middle;
+}
+
+#search i {
+  margin-right: 0.3em;
+  user-select: none;
+}
+
+#search input {
+  width: 100%;
+  border: 0;
+  outline: 0;
+  background-color: transparent;
+}
+
+#search.active div {
+  visibility: visible;
+  opacity: 1;
+  top: 100%;
+}
+
+#search ul {
+  padding: 0;
+  margin: 0;
+  list-style: none;
+}
+
+#search li {
+  margin-bottom: .5em;
+}
+
+#search>div {
+  position: absolute;
+  top: 0;
+  width: 100%;
+  left: 0;
+  z-index: 999999;
+  background-color: #fff;
+  text-align: left;
+  color: #ccc;
+  box-shadow: 0 2px 3px rgba(0, 0, 0, .06), 0 2px 2px rgba(0, 0, 0, .12);
+  padding: .5em;
+  border-bottom-left-radius: .3em;
+  border-bottom-right-radius: .3em;
+  transition: .1s ease all;
+  visibility: hidden;
+  opacity: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  max-height: 50vh;
+}
+
+#search>div div {
+  white-space: pre-wrap;
+  white-space: -moz-pre-wrap;
+  white-space: -pre-wrap;
+  white-space: -o-pre-wrap;
+  word-wrap: break-word;
+}
+
+#search>div p {
+  width: 100%;
+  text-align: center;
+  display: none;
+  margin: 0;
+  max-width: none;
+}
+
+#search.ongoing p {
+  display: block;
+}
+
+#search.active div i,
+#sidebar #search.active div i {
+  color: #ccc;
+  text-align: center;
+  margin: 0 auto;
+  display: table;
+}
+
+#search::-webkit-input-placeholder {
+  color: rgba(255, 255, 255, .5);
+}
+
+#search:-moz-placeholder {
+  opacity: 1;
+  color: rgba(255, 255, 255, .5);
+}
+
+#search::-moz-placeholder {
+  opacity: 1;
+  color: rgba(255, 255, 255, .5);
+}
+
+#search:-ms-input-placeholder {
+  color: rgba(255, 255, 255, .5);
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *          BOTTOM BAR         *
+ * * * * * * * * * * * * * * * */
+
+#bottom-bar {
+  background-color: #fafafa;
+  border-top: 1px solid rgba(0, 0, 0, 0.075);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.075);
+  height: 3.8em;
+}
+
+#bottom-bar>div:first-child>* {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+#bottom-bar>div:first-child>i {
+  margin-right: .3em;
+}
+
+#bottom-bar>*:first-child {
+  margin-right: auto;
+  max-width: calc(100% - 25em);
+  width: 100%;
+}
+
+#bottom-bar p {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  width: calc(100% - 3em);
+  white-space: nowrap;
+}
+
+#more {
+  display: none;
+}
+
+#file-only {
+  display: inline-block;
+  border-right: 1px solid rgba(0, 0, 0, 0.075);
+  padding-right: .3em;
+  margin-right: .3em;
+  transition: .2s ease opacity, visibility;
+  visibility: visible;
+}
+
+#file-only.disabled {
+  opacity: 0;
+  visibility: hidden;
+}
+
+#download ul.active {
+  top: 0;
+  right: 0;
+}
+
+#more ul.active {
+  right: .5em;
+  top: 4.5em;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            DROPDOWN         *
+ * * * * * * * * * * * * * * * */
+
+.dropdown {
+  position: fixed;
+  top: -100%;
+  right: -100%;
+  visibility: hidden;
+  display: flex;
+  flex-direction: column;
+  border-radius: .1em;
+  border-top-left-radius: 0;
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+  background: #fff;
+  z-index: 9999999;
+}
+
+.dropdown.active {
+  visibility: visible;
+}
+
+.dropdown .action {
+  padding: .7em;
+}
+
+.dropdown i {
+  padding: 0;
+  vertical-align: middle;
+}
+
+.dropdown span {
+  display: inline-block;
+  margin-left: .5em;
+  font-size: .9em;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *          BREADCRUMBS        *
+ * * * * * * * * * * * * * * * */
+
+#previous {
+  margin-left: -.5em;
+}
+
+#breadcrumbs {
+  min-width: 7em;
+}
+
+#breadcrumbs.active {
+  top: 0;
+  left: 0;
+  right: auto;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            LISTING          *
+ * * * * * * * * * * * * * * * */
+
+#listing {
+  max-width: calc(100% - 1.2em);
+  width: 100%;
+}
+
+#listing h2 {
+  margin: 0 0 0 0.5em;
+  font-size: 1em;
+  color: rgba(0, 0, 0, 0.2);
+  font-weight: 500;
+}
+
+#listing .item div:last-of-type * {
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+#listing>div {
+  display: flex;
+  padding: 0;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  position: relative;
+}
+
+#listing .item {
+  background-color: #fff;
+  position: relative;
+  display: flex;
+  flex-wrap: nowrap;
+  color: #6f6f6f;
+  transition: .1s ease all;
+  align-items: center;
+  cursor: pointer;
+}
+
+#listing .item div:last-of-type {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+#listing .item p {
+  margin: 0;
+}
+
+#listing .item .size,
+#listing .item .modified {
+  font-size: 0.9em;
+}
+
+#listing .item .name {
+  font-weight: bold;
+}
+
+#listing .item i {
+  font-size: 4em;
+  margin-right: 0.1em;
+  vertical-align: bottom;
+}
+
+#listing h2.message,
+.message {
+  text-align: center;
+  font-size: 3em;
+  margin: 1em auto;
+  display: block !important;
+  width: 95%;
+  color: rgba(0, 0, 0, 0.2);
+  font-weight: 500;
+}
+
+.message i {
+  font-size: inherit;
+  vertical-align: middle;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *        LISTING - MOSAIC     *
+ * * * * * * * * * * * * * * * */
+
+#listing.mosaic {
+  margin-top: 1em;
+}
+
+#listing.mosaic .item {
+  width: calc(33% - 1em);
+  margin: .5em;
+  padding: 0.5em;
+  border-radius: 0.2em;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
+}
+
+#listing.mosaic .item:hover {
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
+}
+
+#listing.mosaic .header {
+  display: none;
+}
+
+#listing.mosaic .item div:first-of-type {
+  width: 5em;
+}
+
+#listing.mosaic .item div:last-of-type {
+  width: calc(100% - 5vw);
+}
+
+
+/* * * * * * * * * * * * * * * *
+  *        LISTING - DETAIL     *
+  * * * * * * * * * * * * * * * */
+
+#listing.list {
+  flex-direction: column;
+  padding-top: 3.25em;
+  width: 100%;
+  max-width: 100%;
+  margin: 0;
+}
+
+#listing.list .item {
+  width: 100%;
+  margin: 0;
+  border: 0;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+  padding: 1em;
+}
+
+#listing.list h2 {
+  display: none;
+}
+
+#listing .item[aria-selected=true] {
+  background: #2196f3 !important;
+  color: #fff !important;
+}
+
+#listing.list .item div:first-of-type {
+  width: 3em;
+}
+
+#listing.list .item div:first-of-type i {
+  font-size: 2em;
+}
+
+#listing.list .item div:last-of-type {
+  width: calc(100% - 3em);
+  display: flex;
+  align-items: center;
+}
+
+#listing.list .item .name {
+  width: 50%;
+}
+
+#listing.list .item .size {
+  width: 25%;
+}
+
+#listing .item.header {
+  display: none !important;
+  background-color: #ccc;
+}
+
+#listing.list .header i {
+  font-size: 1.5em;
+  vertical-align: middle;
+  margin-left: .2em;
+}
+
+#listing.list .item.header {
+  display: flex !important;
+  background: #fafafa;
+  position: fixed;
+  width: 100%;
+  top: 7.8em;
+  left: 0;
+  z-index: 999;
+  padding: .85em;
+}
+
+#listing.list .item.header>div:first-child {
+  width: 0;
+}
+
+#listing.list .item.header .name {
+  margin-right: 3em;
+}
+
+#listing.list .header {
+  display: flex;
+  background: #fafafa;
+  position: fixed;
+  width: 100%;
+  top: 7.8em;
+  left: 0;
+  z-index: 999;
+}
+
+#listing.list .header a {
+  color: inherit;
+}
+
+#listing.list .item.header>div:first-child {
+  width: 0;
+}
+
+#listing.list .name {
+  font-weight: normal;
+}
+
+#listing.list .item.header .name {
+  margin-right: 3em;
+}
+
+#listing.list .header span {
+  vertical-align: middle;
+}
+
+#listing.list .header i {
+  opacity: 0;
+  transition: .1s ease all;
+}
+
+#listing.list .header p:hover i,
+#listing.list .header .active i {
+  opacity: 1;
+}
+
+#listing.list .item.header .active {
+  font-weight: bold;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *  MULTIPLE SELECTION DIALOG  *
+ * * * * * * * * * * * * * * * */
+
+#multiple-selection {
+  position: fixed;
+  bottom: -4em;
+  left: 0;
+  z-index: 99999999;
+  width: 100%;
+  background-color: #2196f3;
+  height: 4em;
+  display: flex !important;
+  padding: 0.5em 0.5em 0.5em 1em;
+  justify-content: space-between;
+  align-items: center;
+  transition: .2s ease all;
+}
+
+#multiple-selection.active {
+  bottom: 0;
+}
+
+#multiple-selection * {
+  margin: 0;
+}
+
+#multiple-selection p,
+#multiple-selection i {
+  color: #fff;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            PROMPT           *
+ * * * * * * * * * * * * * * * */
+
+.overlay,
+.prompt,
+.help {
+  opacity: 0;
+  z-index: -1;
+  transition: .1s ease opacity, z-index;
+}
+
+.overlay.active,
+.prompt.active,
+.help.active {
+  z-index: 9999999;
+  opacity: 1;
+}
+
+.overlay {
+  background-color: rgba(0, 0, 0, 0.5);
+  position: fixed;
+  top: 0;
+  left: 0;
+  height: 0;
+  width: 0;
+}
+
+.overlay.active {
+  height: 100%;
+  width: 100%;
+}
+
+.prompt,
+.help {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 99999999;
+  background: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.075);
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+  padding: 2em;
+  max-width: 25em;
+  width: 90%;
+  max-height: 95%;
+}
+
+.prompt h3,
+.help h3 {
+  margin: 0;
+  font-weight: 500;
+  font-size: 1.5em;
+}
+
+.prompt p,
+.help p {
+  font-size: .9em;
+  color: rgba(0, 0, 0, 0.8);
+  margin: .5em 0 1em;
+}
+
+.prompt input {
+  width: 100%;
+  border: 1px solid #dadada;
+  line-height: 1;
+  padding: .3em;
+}
+
+.prompt code {
+  word-wrap: break-word;
+}
+
+.prompt div,
+.help div {
+  margin-top: 1em;
+  display: flex;
+  justify-content: flex-start;
+  flex-direction: row-reverse;
+}
+
+.prompt .cancel {
+  background-color: #ECEFF1;
+  color: #37474F;
+}
+
+.prompt .cancel:hover {
+  background-color: #e9eaeb;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *        PROMPT - MOVE        *
+ * * * * * * * * * * * * * * * */
+
+.prompt .file-list {
+  flex-direction: initial;
+  max-height: 50vh;
+  overflow: auto;
+}
+
+.prompt .file-list ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+}
+
+.prompt .file-list ul li {
+  width: 100%;
+  user-select: none;
+}
+
+.prompt .file-list ul li[aria-selected=true] {
+  background: #2196f3 !important;
+  color: #fff !important;
+  transition: .1s ease all;
+}
+
+.prompt .file-list ul li:hover {
+  background-color: #e9eaeb;
+  cursor: pointer;
+}
+
+.prompt .file-list ul li:before {
+  content: "folder";
+  color: #6f6f6f;
+  vertical-align: middle;
+  padding: 0 .25em;
+  line-height: 2em;
+}
+
+.prompt .file-list ul li[aria-selected=true]:before {
+  color: white;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *             HELP            *
+ * * * * * * * * * * * * * * * */
+
+.help {
+  max-width: 24em;
+  visibility: hidden;
+  top: -100%;
+  left: -100%;
+}
+
+.help.active {
+  visibility: visible;
+  top: 50%;
+  left: 50%;
+}
+
+.help ul {
+  padding: 0;
+  margin: 1em 0;
+  list-style: none;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *            FOOTER           *
+ * * * * * * * * * * * * * * * */
+
+footer {
+  font-size: 0.6em;
+  margin: 2em 0 2em;
+  text-align: center;
+  color: grey;
+}
+
+footer a,
+footer a:hover {
+  color: inherit;
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *        MEDIA QUERIES        *
+ * * * * * * * * * * * * * * * */
+
+@media screen and (max-width: 850px) {
+  .frontmatter {
+    column-count: 2;
+  }
+}
+
+@media screen and (max-width: 650px) {
+  body {
+    transition: .2s ease padding;
+  }
+  .mobile-only {
+    display: inherit !important;
+  }
+  #top-bar>div:nth-child(1) {
+    display: none;
+  }
+  #bottom-bar>*:first-child {
+    max-width: calc(100% - 16em) !important;
+  }
+  #main-actions {
+    position: fixed;
+    top: -100%;
+    right: -100%;
+    visibility: hidden;
+    display: flex;
+    flex-direction: column;
+    border-radius: .1em;
+    border-top-left-radius: 0;
+    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+    background: #fff;
+    z-index: 9999999;
+  }
+  #main-actions.active {
+    right: .5em;
+    top: 4.5em;
+    visibility: visible;
+  }
+  #main-actions .action {
+    padding: .7em;
+    border-radius: 0;
+    align-items: center;
+  }
+  #main-actions .action:hover {
+    background-color: rgba(0, 0, 0, 0.04);
+  }
+  #main-actions i {
+    padding: 0;
+    vertical-align: middle;
+  }
+  #main-actions .action:hover i {
+    padding: 0;
+    background-color: transparent;
+  }
+  #main-actions span {
+    display: inline-block;
+    margin-left: .5em;
+    font-size: .9em;
+  }
+  #listing.list .item .size,
+  #listing.list .item .modified {
+    display: none;
+  }
+  #listing.list .item .name {
+    width: 100%;
+  }
+  .frontmatter {
+    column-count: 1;
+  }
+}
+
+@media screen and (max-width: 450px) {
+  #bottom-bar p {
+    display: none !important;
+  }
+}
+
+
+/* * * * * * * * * * * * * * * *
+ *          ANIMATIONS         *
+ * * * * * * * * * * * * * * * */
+
+@keyframes spin {
+  100% {
+    -webkit-transform: rotate(-360deg);
+    transform: rotate(-360deg);
+  }
+}
diff --git a/_assets/js/common.js b/_assets/js/common.js
new file mode 100644
index 00000000..76cae2a3
--- /dev/null
+++ b/_assets/js/common.js
@@ -0,0 +1,685 @@
+'use strict'
+
+var tempID = '_fm_internal_temporary_id'
+var ssl = (window.location.protocol === 'https:')
+var templates = {}
+var selectedItems = []
+var overlay
+var clickOverlay
+
+// Removes an element, if exists, from an array
+Array.prototype.removeElement = function (element) {
+  var i = this.indexOf(element)
+  if (i !== -1) {
+    this.splice(i, 1)
+  }
+}
+
+// Replaces an element inside an array by another
+Array.prototype.replaceElement = function (oldElement, newElement) {
+  var i = this.indexOf(oldElement)
+  if (i !== -1) {
+    this[i] = newElement
+  }
+}
+
+// Sends a costum event to itself
+Document.prototype.sendCostumEvent = function (text) {
+  this.dispatchEvent(new window.CustomEvent(text))
+}
+
+// Gets the content of a cookie
+Document.prototype.getCookie = function (name) {
+  var re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
+  return document.cookie.replace(re, '$1')
+}
+
+// Remove the last directory of an url
+var removeLastDirectoryPartOf = function (url) {
+  var arr = url.split('/')
+  if (arr.pop() === '') {
+    arr.pop()
+  }
+  return (arr.join('/'))
+}
+
+function getCSSRule (rules) {
+  for (let i = 0; i < rules.length; i++) {
+    rules[i] = rules[i].toLowerCase()
+  }
+
+  let result = null
+  let find = Array.prototype.find
+
+  find.call(document.styleSheets, styleSheet => {
+    result = find.call(styleSheet.cssRules, cssRule => {
+      let found = false
+
+      if (cssRule instanceof CSSStyleRule) {
+        for (let i = 0; i < rules.length; i++) {
+          if (cssRule.selectorText.toLowerCase() === rules[i]) {
+            found = true
+          }
+        }
+      }
+
+      return found
+    })
+
+    return result != null
+  })
+
+  return result
+}
+
+/* * * * * * * * * * * * * * * *
+ *                             *
+ *            BUTTONS          *
+ *                             *
+ * * * * * * * * * * * * * * * */
+var buttons = {
+  previousState: {}
+}
+
+buttons.setLoading = function (name) {
+  if (typeof this[name] === 'undefined') return
+  let i = this[name].querySelector('i')
+
+  this.previousState[name] = i.innerHTML
+  i.style.opacity = 0
+
+  setTimeout(function () {
+    i.classList.add('spin')
+    i.innerHTML = 'autorenew'
+    i.style.opacity = 1
+  }, 200)
+}
+
+// Changes an element to done animation
+buttons.setDone = function (name, success = true) {
+  let i = this[name].querySelector('i')
+
+  i.style.opacity = 0
+
+  let thirdStep = () => {
+    i.innerHTML = this.previousState[name]
+    i.style.opacity = null
+
+    if (selectedItems.length === 0 && document.getElementById('listing')) {
+      document.sendCostumEvent('changed-selected')
+    }
+  }
+
+  let secondStep = () => {
+    i.style.opacity = 0
+    setTimeout(thirdStep, 200)
+  }
+
+  let firstStep = () => {
+    i.classList.remove('spin')
+    i.innerHTML = success
+      ? 'done'
+      : 'close'
+    i.style.opacity = 1
+    setTimeout(secondStep, 1000)
+  }
+
+  setTimeout(firstStep, 200)
+  return false
+}
+
+/* * * * * * * * * * * * * * * *
+ *                             *
+ *            WEBDAV           *
+ *                             *
+ * * * * * * * * * * * * * * * */
+var webdav = {}
+
+webdav.convertURL = function (url) {
+  return window.location.origin + url.replace(baseURL + '/', webdavURL + '/')
+}
+
+webdav.move = function (oldLink, newLink) {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    let destination = newLink.replace(baseURL + '/', webdavURL + '/')
+
+    destination = window.location.origin + destination.substring(prefixURL.length)
+
+    request.open('MOVE', webdav.convertURL(oldLink), true)
+    request.setRequestHeader('Destination', destination)
+    request.onload = () => {
+      if (request.status === 201 || request.status === 204) {
+        resolve()
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send()
+  })
+}
+
+webdav.put = function (link, body, headers = {}) {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    request.open('PUT', webdav.convertURL(link), true)
+
+    for (let key in headers) {
+      request.setRequestHeader(key, headers[key])
+    }
+
+    request.onload = () => {
+      if (request.status == 201) {
+        resolve()
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send(body)
+  })
+}
+
+webdav.propfind = function (link, body, headers = {}) {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    request.open('PROPFIND', webdav.convertURL(link), true)
+
+    for (let key in headers) {
+      request.setRequestHeader(key, headers[key])
+    }
+
+    request.onload = () => {
+      if (request.status < 300) {
+        resolve(request.responseText)
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send(body)
+  })
+}
+
+webdav.delete = function (link) {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    request.open('DELETE', webdav.convertURL(link), true)
+    request.onload = () => {
+      if (request.status === 204) {
+        resolve()
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send()
+  })
+}
+
+webdav.new = function (link) {
+  return new Promise((resolve, reject) => {
+    let request = new window.XMLHttpRequest()
+    request.open((link.endsWith('/') ? 'MKCOL' : 'PUT'), webdav.convertURL(link), true)
+    request.onload = () => {
+      if (request.status === 201) {
+        resolve()
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send()
+  })
+}
+
+/* * * * * * * * * * * * * * * *
+ *                             *
+ *            EVENTS           *
+ *                             *
+ * * * * * * * * * * * * * * * */
+function closePrompt (event) {
+  let prompt = document.querySelector('.prompt')
+
+  if (!prompt) return
+
+  if (typeof event !== 'undefined') {
+    event.preventDefault()
+  }
+
+  document.querySelector('.overlay').classList.remove('active')
+  prompt.classList.remove('active')
+
+  setTimeout(() => {
+    prompt.remove()
+  }, 100)
+}
+
+function notImplemented (event) {
+  event.preventDefault()
+  clickOverlay.click()
+
+  let clone = document.importNode(templates.message.content, true)
+  clone.querySelector('h3').innerHTML = 'Not implemented'
+  clone.querySelector('p').innerHTML = "Sorry, but this feature wasn't implemented yet."
+
+  document.querySelector('body').appendChild(clone)
+  document.querySelector('.overlay').classList.add('active')
+  document.querySelector('.prompt').classList.add('active')
+}
+
+// Prevent Default event
+var preventDefault = function (event) {
+  event.preventDefault()
+}
+
+function logoutEvent (event) {
+  let request = new window.XMLHttpRequest()
+  request.open('GET', window.location.pathname, true, 'username', 'password')
+  request.send()
+  request.onreadystatechange = function () {
+    if (request.readyState === 4) {
+      window.location = '/'
+    }
+  }
+}
+
+function openEvent (event) {
+  if (event.currentTarget.classList.contains('disabled')) {
+    return false
+  }
+
+  let link = '?raw=true'
+
+  if (selectedItems.length) {
+    link = document.getElementById(selectedItems[0]).dataset.url + link
+  } else {
+    link = window.location.pathname + link
+  }
+
+  window.open(link)
+  return false
+}
+
+function getHash (event, hash) {
+  event.preventDefault()
+
+  let request = new window.XMLHttpRequest()
+  let link
+
+  if (selectedItems.length) {
+    link = document.getElementById(selectedItems[0]).dataset.url
+  } else {
+    link = window.location.pathname
+  }
+
+  request.open('GET', `${link}?checksum=${hash}`, true)
+
+  request.onload = () => {
+    if (request.status >= 300) {
+      console.log(request.statusText)
+      return
+    }
+    event.target.parentElement.innerHTML = request.responseText
+  }
+  request.onerror = (e) => console.log(e)
+  request.send()
+}
+
+function infoEvent (event) {
+  event.preventDefault()
+  if (event.currentTarget.classList.contains('disabled')) {
+    return
+  }
+
+  let dir = false
+  let link
+
+  if (selectedItems.length) {
+    link = document.getElementById(selectedItems[0]).dataset.url
+    dir = document.getElementById(selectedItems[0]).dataset.dir
+  } else {
+    if (document.getElementById('listing') !== null) {
+      dir = true
+    }
+
+    link = window.location.pathname
+  }
+
+  buttons.setLoading('info', false)
+
+  webdav.propfind(link)
+    .then((text) => {
+      let parser = new window.DOMParser()
+      let xml = parser.parseFromString(text, 'text/xml')
+      let clone = document.importNode(templates.info.content, true)
+
+      let value = xml.getElementsByTagName('displayname')
+      if (value.length > 0) {
+        clone.getElementById('display_name').innerHTML = value[0].innerHTML
+      } else {
+        clone.getElementById('display_name').innerHTML = xml.getElementsByTagName('D:displayname')[0].innerHTML
+      }
+
+      value = xml.getElementsByTagName('getcontentlength')
+      if (value.length > 0) {
+        clone.getElementById('content_length').innerHTML = value[0].innerHTML
+      } else {
+        clone.getElementById('content_length').innerHTML = xml.getElementsByTagName('D:getcontentlength')[0].innerHTML
+      }
+
+      value = xml.getElementsByTagName('getlastmodified')
+      if (value.length > 0) {
+        clone.getElementById('last_modified').innerHTML = value[0].innerHTML
+      } else {
+        clone.getElementById('last_modified').innerHTML = xml.getElementsByTagName('D:getlastmodified')[0].innerHTML
+      }
+
+      if (dir === true || dir === 'true') {
+        clone.querySelector('.file-only').style.display = 'none'
+      }
+
+      document.querySelector('body').appendChild(clone)
+      document.querySelector('.overlay').classList.add('active')
+      document.querySelector('.prompt').classList.add('active')
+      buttons.setDone('info', true)
+    })
+    .catch(e => {
+      buttons.setDone('info', false)
+      console.log(e)
+    })
+}
+
+function deleteOnSingleFile () {
+  closePrompt()
+  buttons.setLoading('delete')
+
+  webdav.delete(window.location.pathname)
+    .then(() => {
+      window.location.pathname = removeLastDirectoryPartOf(window.location.pathname)
+    })
+    .catch(e => {
+      buttons.setDone('delete', false)
+      console.log(e)
+    })
+}
+
+function deleteOnListing () {
+  closePrompt()
+  buttons.setLoading('delete')
+
+  let promises = []
+
+  for (let id of selectedItems) {
+    promises.push(webdav.delete(document.getElementById(id).dataset.url))
+  }
+
+  Promise.all(promises)
+    .then(() => {
+      listing.reload()
+      buttons.setDone('delete')
+    })
+    .catch(e => {
+      console.log(e)
+      buttons.setDone('delete', false)
+    })
+}
+
+// Handles the delete button event
+function deleteEvent (event) {
+  let single = false
+
+  if (!selectedItems.length) {
+    selectedItems = ['placeholder']
+    single = true
+  }
+
+  let clone = document.importNode(templates.question.content, true)
+  clone.querySelector('h3').innerHTML = 'Delete files'
+
+  if (single) {
+    clone.querySelector('form').addEventListener('submit', deleteOnSingleFile)
+    clone.querySelector('p').innerHTML = `Are you sure you want to delete this file/folder?`
+  } else {
+    clone.querySelector('form').addEventListener('submit', deleteOnListing)
+    clone.querySelector('p').innerHTML = `Are you sure you want to delete ${selectedItems.length} file(s)?`
+  }
+
+  clone.querySelector('input').remove()
+  clone.querySelector('.ok').innerHTML = 'Delete'
+
+  document.body.appendChild(clone)
+  document.querySelector('.overlay').classList.add('active')
+  document.querySelector('.prompt').classList.add('active')
+
+  return false
+}
+
+function resetSearchText () {
+  let box = document.querySelector('#search > div div')
+
+  if (user.AllowCommands) {
+    box.innerHTML = `Search or use one of your supported commands: ${user.Commands.join(", ")}.`
+  } else {
+    box.innerHTML = 'Type and press enter to search.'
+  }
+}
+
+function searchEvent (event) {
+  if (this.value.length === 0) {
+    resetSearchText()
+    return
+  }
+
+  let value = this.value,
+    search = document.getElementById('search'),
+    scrollable = document.querySelector('#search > div'),
+    box = document.querySelector('#search > div div'),
+    pieces = value.split(' '),
+    supported = false
+
+  user.Commands.forEach(function (cmd) {
+    if (cmd == pieces[0]) {
+      supported = true
+    }
+  })
+
+  if (!supported || !user.AllowCommands) {
+    box.innerHTML = 'Press enter to search.'
+  } else {
+    box.innerHTML = 'Press enter to execute.'
+  }
+
+  if (event.keyCode === 13) {
+    box.innerHTML = ''
+    search.classList.add('ongoing')
+
+    let url = window.location.host + window.location.pathname
+
+    if (document.getElementById('editor')) {
+      url = removeLastDirectoryPartOf(url)
+    }
+
+    let protocol = ssl ? 'wss:' : 'ws:'
+
+    if (supported && user.AllowCommands) {
+      let conn = new window.WebSocket(`${protocol}//${url}?command=true`)
+
+      conn.onopen = function () {
+        conn.send(value)
+      }
+
+      conn.onmessage = function (event) {
+        box.innerHTML = event.data
+        scrollable.scrollTop = scrollable.scrollHeight
+      }
+
+      conn.onclose = function (event) {
+        search.classList.remove('ongoing')
+        listing.reload()
+      }
+
+      return
+    }
+
+    box.innerHTML = '<ul></ul>'
+
+    let ul = box.querySelector('ul')
+    let conn = new window.WebSocket(`${protocol}//${url}?search=true`)
+
+    conn.onopen = function () {
+      conn.send(value)
+    }
+
+    conn.onmessage = function (event) {
+      ul.innerHTML += '<li><a href="' + event.data + '">' + event.data + '</a></li>'
+      scrollable.scrollTop = scrollable.scrollHeight
+    }
+
+    conn.onclose = function (event) {
+      search.classList.remove('ongoing')
+    }
+  }
+}
+
+function setupSearch () {
+  let search = document.getElementById('search')
+  let searchInput = search.querySelector('input')
+  let searchDiv = search.querySelector('div')
+  let hover = false
+  let focus = false
+
+  resetSearchText()
+
+  searchInput.addEventListener('focus', event => {
+    focus = true
+    search.classList.add('active')
+  })
+
+  searchDiv.addEventListener('mouseover', event => {
+    hover = true
+    search.classList.add('active')
+  })
+
+  searchInput.addEventListener('blur', event => {
+    focus = false
+    if (hover) return
+    search.classList.remove('active')
+  })
+
+  search.addEventListener('mouseleave', event => {
+    hover = false
+    if (focus) return
+    search.classList.remove('active')
+  })
+
+  search.addEventListener('click', event => {
+    search.classList.add('active')
+    search.querySelector('input').focus()
+  })
+
+  searchInput.addEventListener('keyup', searchEvent)
+}
+
+function closeHelp (event) {
+  event.preventDefault()
+
+  document.querySelector('.help').classList.remove('active')
+  document.querySelector('.overlay').classList.remove('active')
+}
+
+function openHelp (event) {
+  closePrompt(event)
+
+  document.querySelector('.help').classList.add('active')
+  document.querySelector('.overlay').classList.add('active')
+}
+
+window.addEventListener('keydown', (event) => {
+  if (event.keyCode === 27) {
+    if (document.querySelector('.help.active')) {
+      closeHelp(event)
+    }
+  }
+
+  if (event.keyCode === 46) {
+    deleteEvent(event)
+  }
+
+  if (event.keyCode === 112) {
+    event.preventDefault()
+    openHelp(event)
+  }
+})
+
+/* * * * * * * * * * * * * * * *
+ *                             *
+ *           BOOTSTRAP         *
+ *                             *
+ * * * * * * * * * * * * * * * */
+
+document.addEventListener('DOMContentLoaded', function (event) {
+  overlay = document.querySelector('.overlay')
+  clickOverlay = document.querySelector('#click-overlay')
+
+  buttons.logout = document.getElementById('logout')
+  buttons.open = document.getElementById('open')
+  buttons.delete = document.getElementById('delete')
+  buttons.previous = document.getElementById('previous')
+  buttons.info = document.getElementById('info')
+
+  // Attach event listeners
+  buttons.logout.addEventListener('click', logoutEvent)
+  buttons.open.addEventListener('click', openEvent)
+  buttons.info.addEventListener('click', infoEvent)
+
+  templates.question = document.querySelector('#question-template')
+  templates.info = document.querySelector('#info-template')
+  templates.message = document.querySelector('#message-template')
+  templates.move = document.querySelector('#move-template')
+
+  if (user.AllowEdit) {
+    buttons.delete.addEventListener('click', deleteEvent)
+  }
+
+  let dropdownButtons = document.querySelectorAll('.action[data-dropdown]')
+  Array.from(dropdownButtons).forEach(button => {
+    button.addEventListener('click', event => {
+      button.querySelector('ul').classList.toggle('active')
+      clickOverlay.classList.add('active')
+
+      clickOverlay.addEventListener('click', event => {
+        button.querySelector('ul').classList.remove('active')
+        clickOverlay.classList.remove('active')
+      })
+    })
+  })
+
+  overlay.addEventListener('click', event => {
+    if (document.querySelector('.help.active')) {
+      closeHelp(event)
+      return
+    }
+
+    closePrompt(event)
+  })
+
+  let mainActions = document.getElementById('main-actions')
+
+  document.getElementById('more').addEventListener('click', event => {
+    event.preventDefault()
+    event.stopPropagation()
+
+    clickOverlay.classList.add('active')
+    mainActions.classList.add('active')
+
+    clickOverlay.addEventListener('click', event => {
+      mainActions.classList.remove('active')
+      clickOverlay.classList.remove('active')
+    })
+  })
+
+  setupSearch()
+  return false
+})
diff --git a/_assets/js/editor.js b/_assets/js/editor.js
new file mode 100644
index 00000000..38ff40c7
--- /dev/null
+++ b/_assets/js/editor.js
@@ -0,0 +1,278 @@
+'use strict'
+
+var editor = {}
+
+editor.textareaAutoGrow = function () {
+  let autogrow = function () {
+    console.log(this.style.height)
+    this.style.height = 'auto'
+    this.style.height = (this.scrollHeight) + 'px'
+  }
+
+  let textareas = document.getElementsByTagName('textarea')
+
+  let addAutoGrow = () => {
+    Array.from(textareas).forEach(textarea => {
+      autogrow.bind(textarea)()
+      textarea.addEventListener('keyup', autogrow)
+    })
+  }
+
+  addAutoGrow()
+  window.addEventListener('resize', addAutoGrow)
+}
+
+editor.toggleSourceEditor = function (event) {
+  event.preventDefault()
+
+  if (document.querySelector('[data-kind="content-only"]')) {
+    window.location = window.location.pathname + '?visual=true'
+    return
+  }
+
+  window.location = window.location.pathname + '?visual=false'
+}
+
+function deleteFrontMatterItem (event) {
+  event.preventDefault()
+  document.getElementById(this.dataset.delete).remove()
+}
+
+function makeFromBaseTemplate (id, type, name, parent) {
+  let clone = document.importNode(templates.base.content, true)
+  clone.querySelector('fieldset').id = id
+  clone.querySelector('fieldset').dataset.type = type
+  clone.querySelector('h3').innerHTML = name
+  clone.querySelector('.delete').dataset.delete = id
+  clone.querySelector('.delete').addEventListener('click', deleteFrontMatterItem)
+  clone.querySelector('.add').addEventListener('click', addFrontMatterItem)
+
+  if (parent.classList.contains('frontmatter')) {
+    parent.insertBefore(clone, document.querySelector('div.button.add'))
+    return
+  }
+
+  parent.appendChild(clone)
+}
+
+function makeFromArrayItemTemplate (id, number, parent) {
+  let clone = document.importNode(templates.arrayItem.content, true)
+  clone.querySelector('[data-type="array-item"]').id = `${id}-${number}`
+  clone.querySelector('input').name = id
+  clone.querySelector('input').id = id
+  clone.querySelector('div.action').dataset.delete = `${id}-${number}`
+  clone.querySelector('div.action').addEventListener('click', deleteFrontMatterItem)
+  parent.querySelector('.group').appendChild(clone)
+  document.getElementById(`${id}-${number}`).querySelector('input').focus()
+}
+
+function makeFromObjectItemTemplate (id, name, parent) {
+  let clone = document.importNode(templates.objectItem.content, true)
+  clone.querySelector('.block').id = `block-${id}`
+  clone.querySelector('.block').dataset.content = id
+  clone.querySelector('label').for = id
+  clone.querySelector('label').innerHTML = name
+  clone.querySelector('input').name = id
+  clone.querySelector('input').id = id
+  clone.querySelector('.action').dataset.delete = `block-${id}`
+  clone.querySelector('.action').addEventListener('click', deleteFrontMatterItem)
+
+  parent.appendChild(clone)
+  document.getElementById(id).focus()
+}
+
+function addFrontMatterItemPrompt (parent) {
+  return function (event) {
+    event.preventDefault()
+
+    let value = event.currentTarget.querySelector('input').value
+    if (value === '') {
+      return true
+    }
+
+    closePrompt(event)
+
+    let name = value.substring(0, value.lastIndexOf(':')),
+      type = value.substring(value.lastIndexOf(':') + 1, value.length)
+
+    if (type !== '' && type !== 'array' && type !== 'object') {
+      name = value
+    }
+
+    name = name.replace(' ', '_')
+
+    let id = name
+
+    if (parent.id != '') {
+      id = parent.id + '.' + id
+    }
+
+    if (type == 'array' || type == 'object') {
+      if (parent.dataset.type == 'parent') {
+        makeFromBaseTemplate(id, type, name, document.querySelector('.frontmatter'))
+        return
+      }
+
+      makeFromBaseTemplate(id, type, name, block)
+      return
+    }
+
+    let group = parent.querySelector('.group')
+
+    if (group == null) {
+      parent.insertAdjacentHTML('afterbegin', '<div class="group"></div>')
+      group = parent.querySelector('.group')
+    }
+
+    makeFromObjectItemTemplate(id, name, group)
+  }
+}
+
+function addFrontMatterItem (event) {
+  event.preventDefault()
+
+  let parent = event.currentTarget.parentNode,
+    type = parent.dataset.type
+
+  // If the block is an array
+  if (type === 'array') {
+    let id = parent.id + '[]',
+      count = parent.querySelectorAll('.group > div').length,
+      fieldsets = parent.getElementsByTagName('fieldset')
+
+    if (fieldsets.length > 0) {
+      let itemType = fieldsets[0].dataset.type,
+        itemID = parent.id + '[' + fieldsets.length + ']',
+        itemName = fieldsets.length
+
+      makeFromBaseTemplate(itemID, itemType, itemName, parent)
+    } else {
+      makeFromArrayItemTemplate(id, count, parent)
+    }
+
+    return
+  }
+
+  if (type == 'object' || type == 'parent') {
+    let clone = document.importNode(templates.question.content, true)
+    clone.querySelector('form').id = tempID
+    clone.querySelector('h3').innerHTML = 'New field'
+    clone.querySelector('p').innerHTML = 'Write the field name and then press enter. If you want to create an array or an object, end the name with <code>:array</code> or <code>:object.</code>'
+    clone.querySelector('.ok').innerHTML = 'Create'
+    clone.querySelector('form').addEventListener('submit', addFrontMatterItemPrompt(parent))
+    clone.querySelector('form').classList.add('active')
+    document.querySelector('body').appendChild(clone)
+
+    document.querySelector('.overlay').classList.add('active')
+    document.getElementById(tempID).classList.add('active')
+  }
+
+  return false
+}
+
+document.addEventListener('DOMContentLoaded', (event) => {
+  if (!document.getElementById('editor')) return
+
+  editor.textareaAutoGrow()
+
+  templates.arrayItem = document.getElementById('array-item-template')
+  templates.base = document.getElementById('base-template')
+  templates.objectItem = document.getElementById('object-item-template')
+  templates.temporary = document.getElementById('temporary-template')
+
+  buttons.save = document.querySelector('#save')
+  buttons.editSource = document.querySelector('#edit-source')
+
+  if (buttons.editSource) {
+    buttons.editSource.addEventListener('click', editor.toggleSourceEditor)
+  }
+
+  let container = document.getElementById('editor'),
+    kind = container.dataset.kind,
+    rune = container.dataset.rune
+
+  if (kind != 'frontmatter-only') {
+    let editor = document.querySelector('.content #ace'),
+      mode = editor.dataset.mode,
+      textarea = document.querySelector('textarea[name="content"]'),
+      aceEditor = ace.edit('ace'),
+      options = {
+        wrap: true,
+        maxLines: Infinity,
+        theme: 'ace/theme/github',
+        showPrintMargin: false,
+        fontSize: '1em',
+        minLines: 20
+    }
+
+    aceEditor.getSession().setMode('ace/mode/' + mode)
+    aceEditor.getSession().setValue(textarea.value)
+    aceEditor.getSession().on('change', function () {
+      textarea.value = aceEditor.getSession().getValue()
+    })
+
+    if (mode == 'markdown') options.showGutter = false
+    aceEditor.setOptions(options)
+  }
+
+  let deleteFrontMatterItemButtons = document.getElementsByClassName('delete')
+  Array.from(deleteFrontMatterItemButtons).forEach(button => {
+    button.addEventListener('click', deleteFrontMatterItem)
+  })
+
+  let addFrontMatterItemButtons = document.getElementsByClassName('add')
+  Array.from(addFrontMatterItemButtons).forEach(button => {
+    button.addEventListener('click', addFrontMatterItem)
+  })
+
+  let saveContent = function () {
+    let data = form2js(document.querySelector('form'))
+
+    if (typeof data.content === 'undefined' && kind !== 'frontmatter-only') {
+      data.content = ''
+    }
+
+    if (typeof data.content === 'number') {
+      data.content = data.content.toString()
+    }
+
+    let request = new XMLHttpRequest()
+
+    buttons.setLoading('save')
+
+    webdav.put(window.location.pathname, JSON.stringify(data), {
+      'Kind': kind,
+      'Rune': rune
+    })
+      .then(() => {
+        buttons.setDone('save')
+      })
+      .catch(e => {
+        console.log(e)
+        buttons.setDone('save', false)
+      })
+  }
+
+  document.querySelector('#save').addEventListener('click', event => {
+    event.preventDefault()
+    saveContent()
+  })
+
+  document.querySelector('form').addEventListener('submit', (event) => {
+    event.preventDefault()
+    saveContent()
+  })
+
+  window.addEventListener('keydown', (event) => {
+    if (event.ctrlKey || event.metaKey) {
+      switch (String.fromCharCode(event.which).toLowerCase()) {
+        case 's':
+          event.preventDefault()
+          saveContent()
+          break
+      }
+    }
+  })
+
+  return false
+})
diff --git a/_assets/js/listing.js b/_assets/js/listing.js
new file mode 100644
index 00000000..baef101d
--- /dev/null
+++ b/_assets/js/listing.js
@@ -0,0 +1,580 @@
+'use strict'
+
+var listing = {
+  selectMultiple: false
+}
+
+listing.reload = function (callback) {
+  let request = new XMLHttpRequest()
+
+  request.open('GET', window.location)
+  request.setRequestHeader('Minimal', 'true')
+  request.send()
+  request.onreadystatechange = function () {
+    if (request.readyState === 4) {
+      if (request.status === 200) {
+        document.querySelector('body main').innerHTML = request.responseText
+        listing.addDoubleTapEvent()
+
+        if (typeof callback === 'function') {
+          callback()
+        }
+      }
+    }
+  }
+}
+
+listing.itemDragStart = function (event) {
+  let el = event.target
+
+  for (let i = 0; i < 5; i++) {
+    if (!el.classList.contains('item')) {
+      el = el.parentElement
+    }
+  }
+
+  event.dataTransfer.setData('id', el.id)
+  event.dataTransfer.setData('name', el.querySelector('.name').innerHTML)
+}
+
+listing.itemDragOver = function (event) {
+  event.preventDefault()
+  let el = event.target
+
+  for (let i = 0; i < 5; i++) {
+    if (!el.classList.contains('item')) {
+      el = el.parentElement
+    }
+  }
+
+  el.style.opacity = 1
+}
+
+listing.itemDrop = function (e) {
+  e.preventDefault()
+
+  let el = e.target,
+    id = e.dataTransfer.getData('id'),
+    name = e.dataTransfer.getData('name')
+
+  if (id == '' || name == '') return
+
+  for (let i = 0; i < 5; i++) {
+    if (!el.classList.contains('item')) {
+      el = el.parentElement
+    }
+  }
+
+  if (el.id === id) return
+
+  let oldLink = document.getElementById(id).dataset.url,
+    newLink = el.dataset.url + name
+
+  webdav.move(oldLink, newLink)
+    .then(() => listing.reload())
+    .catch(e => console.log(e))
+}
+
+listing.documentDrop = function (event) {
+  event.preventDefault()
+  let dt = event.dataTransfer,
+    files = dt.files,
+    el = event.target,
+    items = document.getElementsByClassName('item')
+
+  for (let i = 0; i < 5; i++) {
+    if (el != null && !el.classList.contains('item')) {
+      el = el.parentElement
+    }
+  }
+
+  if (files.length > 0) {
+    if (el != null && el.classList.contains('item') && el.dataset.dir == 'true') {
+      listing.handleFiles(files, el.querySelector('.name').innerHTML + '/')
+      return
+    }
+
+    listing.handleFiles(files, '')
+  } else {
+    Array.from(items).forEach(file => {
+      file.style.opacity = 1
+    })
+  }
+}
+
+listing.rename = function (event) {
+  if (!selectedItems.length || selectedItems.length > 1) {
+    return false
+  }
+
+  let item = document.getElementById(selectedItems[0])
+
+  if (item.classList.contains('disabled')) {
+    return false
+  }
+
+  let link = item.dataset.url,
+    field = item.querySelector('.name'),
+    name = field.innerHTML
+
+  let submit = (event) => {
+    event.preventDefault()
+
+    let newName = event.currentTarget.querySelector('input').value,
+      newLink = removeLastDirectoryPartOf(link) + '/' + newName
+
+    closePrompt(event)
+    buttons.setLoading('rename')
+
+    webdav.move(link, newLink).then(() => {
+      listing.reload(() => {
+        newName = btoa(newName)
+        selectedItems = [newName]
+        document.getElementById(newName).setAttribute('aria-selected', true)
+        listing.handleSelectionChange()
+      })
+
+      buttons.setDone('rename')
+    }).catch(error => {
+      field.innerHTML = name
+      buttons.setDone('rename', false)
+      console.log(error)
+    })
+
+    return false
+  }
+
+  let clone = document.importNode(templates.question.content, true)
+  clone.querySelector('h3').innerHTML = 'Rename'
+  clone.querySelector('input').value = name
+  clone.querySelector('.ok').innerHTML = 'Rename'
+  clone.querySelector('form').addEventListener('submit', submit)
+
+  document.querySelector('body').appendChild(clone)
+  document.querySelector('.overlay').classList.add('active')
+  document.querySelector('.prompt').classList.add('active')
+
+  return false
+}
+
+listing.handleFiles = function (files, base) {
+  buttons.setLoading('upload')
+
+  let promises = []
+
+  for (let file of files) {
+    promises.push(webdav.put(window.location.pathname + base + file.name, file))
+  }
+
+  Promise.all(promises)
+    .then(() => {
+      listing.reload()
+      buttons.setDone('upload')
+    })
+    .catch(e => {
+      console.log(e)
+      buttons.setDone('upload', false)
+    })
+
+  return false
+}
+
+listing.unselectAll = function () {
+  let items = document.getElementsByClassName('item')
+  Array.from(items).forEach(link => {
+    link.setAttribute('aria-selected', false)
+  })
+
+  selectedItems = []
+
+  listing.handleSelectionChange()
+  return false
+}
+
+listing.handleSelectionChange = function (event) {
+  listing.redefineDownloadURLs()
+
+  let selectedNumber = selectedItems.length,
+    fileAction = document.getElementById('file-only')
+
+  if (selectedNumber) {
+    fileAction.classList.remove('disabled')
+
+    if (selectedNumber > 1) {
+      buttons.open.classList.add('disabled')
+      buttons.rename.classList.add('disabled')
+      buttons.info.classList.add('disabled')
+    }
+
+    if (selectedNumber == 1) {
+      if (document.getElementById(selectedItems[0]).dataset.dir == 'true') {
+        buttons.open.classList.add('disabled')
+      } else {
+        buttons.open.classList.remove('disabled')
+      }
+
+      buttons.info.classList.remove('disabled')
+      buttons.rename.classList.remove('disabled')
+    }
+
+    return false
+  }
+
+  buttons.info.classList.remove('disabled')
+  fileAction.classList.add('disabled')
+  return false
+}
+
+listing.redefineDownloadURLs = function () {
+  let files = ''
+
+  for (let i = 0; i < selectedItems.length; i++) {
+    let url = document.getElementById(selectedItems[i]).dataset.url
+    files += url.replace(window.location.pathname, '') + ','
+  }
+
+  files = files.substring(0, files.length - 1)
+  files = encodeURIComponent(files)
+
+  let links = document.querySelectorAll('#download ul a')
+  Array.from(links).forEach(link => {
+    link.href = '?download=' + link.dataset.format + '&files=' + files
+  })
+}
+
+listing.openItem = function (event) {
+  window.location = event.currentTarget.dataset.url
+}
+
+listing.selectItem = function (event) {
+  let el = event.currentTarget
+
+  if (selectedItems.length != 0) event.preventDefault()
+  if (selectedItems.indexOf(el.id) == -1) {
+    if (!event.ctrlKey && !listing.selectMultiple) listing.unselectAll()
+
+    el.setAttribute('aria-selected', true)
+    selectedItems.push(el.id)
+  } else {
+    el.setAttribute('aria-selected', false)
+    selectedItems.removeElement(el.id)
+  }
+
+  listing.handleSelectionChange()
+  return false
+}
+
+listing.newFileButton = function (event) {
+  event.preventDefault()
+
+  let clone = document.importNode(templates.question.content, true)
+  clone.querySelector('h3').innerHTML = 'New file'
+  clone.querySelector('p').innerHTML = 'End with a trailing slash to create a dir.'
+  clone.querySelector('.ok').innerHTML = 'Create'
+  clone.querySelector('form').addEventListener('submit', listing.newFilePrompt)
+
+  document.querySelector('body').appendChild(clone)
+  document.querySelector('.overlay').classList.add('active')
+  document.querySelector('.prompt').classList.add('active')
+}
+
+listing.newFilePrompt = function (event) {
+  event.preventDefault()
+  buttons.setLoading('new')
+
+  let name = event.currentTarget.querySelector('input').value
+
+  webdav.new(window.location.pathname + name)
+    .then(() => {
+      buttons.setDone('new')
+      listing.reload()
+    })
+    .catch(e => {
+      console.log(e)
+      buttons.setDone('new', false)
+    })
+
+  closePrompt(event)
+  return false
+}
+
+listing.updateColumns = function (event) {
+  let columns = Math.floor(document.getElementById('listing').offsetWidth / 300),
+    items = getCSSRule(['#listing.mosaic .item', '.mosaic#listing .item'])
+
+  items.style.width = `calc(${100/columns}% - 1em)`
+}
+
+listing.addDoubleTapEvent = function () {
+  let items = document.getElementsByClassName('item'),
+    touches = {
+      id: '',
+      count: 0
+  }
+
+  Array.from(items).forEach(file => {
+    file.addEventListener('touchstart', event => {
+      if (touches.id != file.id) {
+        touches.id = file.id
+        touches.count = 1
+
+        setTimeout(() => {
+          touches.count = 0
+        }, 300)
+
+        return
+      }
+
+      touches.count++
+
+      if (touches.count > 1) {
+        window.location = file.dataset.url
+      }
+    })
+  })
+}
+
+// Keydown events
+window.addEventListener('keydown', (event) => {
+  if (event.keyCode == 27) {
+    listing.unselectAll()
+
+    if (document.querySelectorAll('.prompt').length) {
+      closePrompt(event)
+    }
+  }
+
+  if (event.keyCode == 113) {
+    listing.rename()
+  }
+
+  if (event.ctrlKey || event.metaKey) {
+    switch (String.fromCharCode(event.which).toLowerCase()) {
+      case 's':
+        event.preventDefault()
+        window.location = '?download=true'
+    }
+  }
+})
+
+window.addEventListener('resize', () => {
+  listing.updateColumns()
+})
+
+listing.selectMoveFolder = function (event) {
+  if (event.target.getAttribute('aria-selected') === 'true') {
+    event.target.setAttribute('aria-selected', false)
+    return
+  } else {
+    if (document.querySelector('.file-list li[aria-selected=true]')) {
+      document.querySelector('.file-list li[aria-selected=true]').setAttribute('aria-selected', false)
+    }
+    event.target.setAttribute('aria-selected', true)
+    return
+  }
+}
+
+listing.getJSON = function (link) {
+  return new Promise((resolve, reject) => {
+    let request = new XMLHttpRequest()
+    request.open('GET', link)
+    request.setRequestHeader('Accept', 'application/json')
+    request.onload = () => {
+      if (request.status == 200) {
+        resolve(request.responseText)
+      } else {
+        reject(request.statusText)
+      }
+    }
+    request.onerror = () => reject(request.statusText)
+    request.send()
+  })
+}
+
+listing.moveMakeItem = function (url, name) {
+  let node = document.createElement('li'),
+    count = 0
+
+  node.dataset.url = url
+  node.innerHTML = name
+  node.setAttribute('aria-selected', false)
+
+  node.addEventListener('dblclick', listing.moveDialogNext)
+  node.addEventListener('click', listing.selectMoveFolder)
+  node.addEventListener('touchstart', event => {
+    count++
+
+    setTimeout(() => {
+      count = 0
+    }, 300)
+
+    if (count > 1) {
+      listing.moveDialogNext(event)
+    }
+  })
+
+  return node
+}
+
+listing.moveDialogNext = function (event) {
+  let request = new XMLHttpRequest(),
+    prompt = document.querySelector('form.prompt.active'),
+    list = prompt.querySelector('div.file-list ul')
+
+  prompt.addEventListener('submit', listing.moveSelected)
+
+  listing.getJSON(event.target.dataset.url)
+    .then((data) => {
+      let dirs = 0
+
+      prompt.querySelector('ul').innerHTML = ''
+      prompt.querySelector('code').innerHTML = event.target.dataset.url
+
+      if (event.target.dataset.url != baseURL + '/') {
+        let node = listing.moveMakeItem(removeLastDirectoryPartOf(event.target.dataset.url) + '/', '..')
+        list.appendChild(node)
+      }
+
+      if (JSON.parse(data) == null) {
+        prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.`
+        return
+      }
+
+      for (let f of JSON.parse(data)) {
+        if (f.IsDir === true) {
+          dirs++
+          list.appendChild(listing.moveMakeItem(f.URL, f.Name))
+        }
+      }
+
+      if (dirs === 0)
+        prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.`
+    })
+    .catch(e => console.log(e))
+}
+
+listing.moveSelected = function (event) {
+  event.preventDefault()
+
+  let promises = []
+  buttons.setLoading('move')
+
+  for (let file of selectedItems) {
+    let fileElement = document.getElementById(file),
+      destFolder = event.target.querySelector('p code').innerHTML
+
+    if (event.currentTarget.querySelector('li[aria-selected=true]') != null) {
+      destFolder = event.currentTarget.querySelector('li[aria-selected=true]').dataset.url
+    }
+
+    let destPath = '/' + destFolder + '/' + fileElement.querySelector('.name').innerHTML
+    destPath = destPath.replace('//', '/')
+
+    promises.push(webdav.move(fileElement.dataset.url, destPath))
+  }
+
+  Promise.all(promises)
+    .then(() => {
+      closePrompt(event)
+      buttons.setDone('move')
+      listing.reload()
+    })
+    .catch(e => {
+      console.log(e)
+    })
+}
+
+listing.moveEvent = function (event) {
+  if (event.currentTarget.classList.contains('disabled'))
+    return
+
+  listing.getJSON(window.location.pathname)
+    .then((data) => {
+      let prompt = document.importNode(templates.move.content, true),
+        list = prompt.querySelector('div.file-list ul'),
+        dirs = 0
+
+      prompt.querySelector('form').addEventListener('submit', listing.moveSelected)
+      prompt.querySelector('code').innerHTML = window.location.pathname
+
+      if (window.location.pathname !== baseURL + '/') {
+        list.appendChild(listing.moveMakeItem(removeLastDirectoryPartOf(window.location.pathname) + '/', '..'))
+      }
+
+      for (let f of JSON.parse(data)) {
+        if (f.IsDir === true) {
+          dirs++
+          list.appendChild(listing.moveMakeItem(f.URL, f.Name))
+        }
+      }
+
+      if (dirs === 0) {
+        prompt.querySelector('p').innerHTML = `There aren't any folders in this directory.`
+      }
+
+      document.body.appendChild(prompt)
+      document.querySelector('.overlay').classList.add('active')
+      document.querySelector('.prompt').classList.add('active')
+    })
+    .catch(e => console.log(e))
+}
+
+document.addEventListener('DOMContentLoaded', event => {
+  listing.updateColumns()
+  listing.addDoubleTapEvent()
+
+  buttons.rename = document.getElementById('rename')
+  buttons.upload = document.getElementById('upload')
+  buttons.new = document.getElementById('new')
+  buttons.download = document.getElementById('download')
+  buttons.move = document.getElementById('move')
+
+  document.getElementById('multiple-selection-activate').addEventListener('click', event => {
+    listing.selectMultiple = true
+    clickOverlay.click()
+
+    document.getElementById('multiple-selection').classList.add('active')
+    document.querySelector('body').style.paddingBottom = '4em'
+  })
+
+  document.getElementById('multiple-selection-cancel').addEventListener('click', event => {
+    listing.selectMultiple = false
+
+    document.querySelector('body').style.paddingBottom = '0'
+    document.getElementById('multiple-selection').classList.remove('active')
+  })
+
+  if (user.AllowEdit) {
+    buttons.move.addEventListener('click', listing.moveEvent)
+    buttons.rename.addEventListener('click', listing.rename)
+  }
+
+  let items = document.getElementsByClassName('item')
+
+  if (user.AllowNew) {
+    buttons.upload.addEventListener('click', (event) => {
+      document.getElementById('upload-input').click()
+    })
+
+    buttons.new.addEventListener('click', listing.newFileButton)
+
+    // Drag and Drop
+    document.addEventListener('dragover', function (event) {
+      event.preventDefault()
+    }, false)
+
+    document.addEventListener('dragenter', (event) => {
+      Array.from(items).forEach(file => {
+        file.style.opacity = 0.5
+      })
+    }, false)
+
+    document.addEventListener('dragend', (event) => {
+      Array.from(items).forEach(file => {
+        file.style.opacity = 1
+      })
+    }, false)
+
+    document.addEventListener('drop', listing.documentDrop, false)
+  }
+})
diff --git a/_assets/js/vendor/ace b/_assets/js/vendor/ace
new file mode 160000
index 00000000..0b01260b
--- /dev/null
+++ b/_assets/js/vendor/ace
@@ -0,0 +1 @@
+Subproject commit 0b01260b38f4db87b2b862e0c7f12e7e2e2905fd
diff --git a/_assets/js/vendor/form2js.js b/_assets/js/vendor/form2js.js
new file mode 100644
index 00000000..2614c194
--- /dev/null
+++ b/_assets/js/vendor/form2js.js
@@ -0,0 +1,356 @@
+/**
+ * Copyright (c) 2010 Maxim Vasiliev
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author Maxim Vasiliev
+ * Date: 09.09.2010
+ * Time: 19:02:33
+ */
+
+
+(function (root, factory)
+{
+	if (typeof exports !== 'undefined' && typeof module !== 'undefined' && module.exports) {
+		// NodeJS
+		module.exports = factory();
+	}
+	else if (typeof define === 'function' && define.amd)
+	{
+		// AMD. Register as an anonymous module.
+		define(factory);
+	}
+	else
+	{
+		// Browser globals
+		root.form2js = factory();
+	}
+}(this, function ()
+{
+	"use strict";
+
+	/**
+	 * Returns form values represented as Javascript object
+	 * "name" attribute defines structure of resulting object
+	 *
+	 * @param rootNode {Element|String} root form element (or it's id) or array of root elements
+	 * @param delimiter {String} structure parts delimiter defaults to '.'
+	 * @param skipEmpty {Boolean} should skip empty text values, defaults to true
+	 * @param nodeCallback {Function} custom function to get node value
+	 * @param useIdIfEmptyName {Boolean} if true value of id attribute of field will be used if name of field is empty
+	 */
+	function form2js(rootNode, delimiter, skipEmpty, nodeCallback, useIdIfEmptyName, getDisabled)
+	{
+		getDisabled = getDisabled ? true : false;
+		if (typeof skipEmpty == 'undefined' || skipEmpty == null) skipEmpty = true;
+		if (typeof delimiter == 'undefined' || delimiter == null) delimiter = '.';
+		if (arguments.length < 5) useIdIfEmptyName = false;
+
+		rootNode = typeof rootNode == 'string' ? document.getElementById(rootNode) : rootNode;
+
+		var formValues = [],
+			currNode,
+			i = 0;
+
+		/* If rootNode is array - combine values */
+		if (rootNode.constructor == Array || (typeof NodeList != "undefined" && rootNode.constructor == NodeList))
+		{
+			while(currNode = rootNode[i++])
+			{
+				formValues = formValues.concat(getFormValues(currNode, nodeCallback, useIdIfEmptyName, getDisabled));
+			}
+		}
+		else
+		{
+			formValues = getFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled);
+		}
+
+		return processNameValues(formValues, skipEmpty, delimiter);
+	}
+
+	/**
+	 * Processes collection of { name: 'name', value: 'value' } objects.
+	 * @param nameValues
+	 * @param skipEmpty if true skips elements with value == '' or value == null
+	 * @param delimiter
+	 */
+	function processNameValues(nameValues, skipEmpty, delimiter)
+	{
+		var result = {},
+			arrays = {},
+			i, j, k, l,
+			value,
+			nameParts,
+			currResult,
+			arrNameFull,
+			arrName,
+			arrIdx,
+			namePart,
+			name,
+			_nameParts;
+
+		for (i = 0; i < nameValues.length; i++)
+		{
+			value = nameValues[i].value;
+
+			if (skipEmpty && (value === '' || value === null)) continue;
+
+			name = nameValues[i].name;
+			_nameParts = name.split(delimiter);
+			nameParts = [];
+			currResult = result;
+			arrNameFull = '';
+
+			for(j = 0; j < _nameParts.length; j++)
+			{
+				namePart = _nameParts[j].split('][');
+				if (namePart.length > 1)
+				{
+					for(k = 0; k < namePart.length; k++)
+					{
+						if (k == 0)
+						{
+							namePart[k] = namePart[k] + ']';
+						}
+						else if (k == namePart.length - 1)
+						{
+							namePart[k] = '[' + namePart[k];
+						}
+						else
+						{
+							namePart[k] = '[' + namePart[k] + ']';
+						}
+
+						arrIdx = namePart[k].match(/([a-z_]+)?\[([a-z_][a-z0-9_]+?)\]/i);
+						if (arrIdx)
+						{
+							for(l = 1; l < arrIdx.length; l++)
+							{
+								if (arrIdx[l]) nameParts.push(arrIdx[l]);
+							}
+						}
+						else{
+							nameParts.push(namePart[k]);
+						}
+					}
+				}
+				else
+					nameParts = nameParts.concat(namePart);
+			}
+
+			for (j = 0; j < nameParts.length; j++)
+			{
+				namePart = nameParts[j];
+
+				if (namePart.indexOf('[]') > -1 && j == nameParts.length - 1)
+				{
+					arrName = namePart.substr(0, namePart.indexOf('['));
+					arrNameFull += arrName;
+
+					if (!currResult[arrName]) currResult[arrName] = [];
+					currResult[arrName].push(value);
+				}
+				else if (namePart.indexOf('[') > -1)
+				{
+					arrName = namePart.substr(0, namePart.indexOf('['));
+					arrIdx = namePart.replace(/(^([a-z_]+)?\[)|(\]$)/gi, '');
+
+					/* Unique array name */
+					arrNameFull += '_' + arrName + '_' + arrIdx;
+
+					/*
+					 * Because arrIdx in field name can be not zero-based and step can be
+					 * other than 1, we can't use them in target array directly.
+					 * Instead we're making a hash where key is arrIdx and value is a reference to
+					 * added array element
+					 */
+
+					if (!arrays[arrNameFull]) arrays[arrNameFull] = {};
+					if (arrName != '' && !currResult[arrName]) currResult[arrName] = [];
+
+					if (j == nameParts.length - 1)
+					{
+						if (arrName == '')
+						{
+							currResult.push(value);
+							arrays[arrNameFull][arrIdx] = convertValue(currResult[currResult.length - 1]);
+						}
+						else
+						{
+							currResult[arrName].push(value);
+							arrays[arrNameFull][arrIdx] = convertValue(currResult[arrName][currResult[arrName].length - 1]);
+						}
+					}
+					else
+					{
+						if (!arrays[arrNameFull][arrIdx])
+						{
+							if ((/^[0-9a-z_]+\[?/i).test(nameParts[j+1])) currResult[arrName].push({});
+							else currResult[arrName].push([]);
+
+							arrays[arrNameFull][arrIdx] = convertValue(currResult[arrName][currResult[arrName].length - 1]);
+						}
+					}
+
+					currResult = convertValue(arrays[arrNameFull][arrIdx]);
+				}
+				else
+				{
+					arrNameFull += namePart;
+
+					if (j < nameParts.length - 1) /* Not the last part of name - means object */
+					{
+						if (!currResult[namePart]) currResult[namePart] = {};
+						currResult = convertValue(currResult[namePart]);
+					}
+					else
+					{
+						currResult[namePart] = convertValue(value);
+					}
+				}
+			}
+		}
+
+		return result;
+	}
+
+    function convertValue(value) {
+        if (value == "true") return true;
+        if (value == "false") return false;
+        if (!isNaN(value)) return parseInt(value);
+        return value;
+    }
+
+    function getFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled)
+    {
+        var result = extractNodeValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled);
+        return result.length > 0 ? result : getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled);
+    }
+
+    function getSubFormValues(rootNode, nodeCallback, useIdIfEmptyName, getDisabled)
+	{
+		var result = [],
+			currentNode = rootNode.firstChild;
+
+		while (currentNode)
+		{
+			result = result.concat(extractNodeValues(currentNode, nodeCallback, useIdIfEmptyName, getDisabled));
+			currentNode = currentNode.nextSibling;
+		}
+
+		return result;
+	}
+
+    function extractNodeValues(node, nodeCallback, useIdIfEmptyName, getDisabled) {
+        if (node.disabled && !getDisabled) return [];
+
+        var callbackResult, fieldValue, result, fieldName = getFieldName(node, useIdIfEmptyName);
+
+        callbackResult = nodeCallback && nodeCallback(node);
+
+        if (callbackResult && callbackResult.name) {
+            result = [callbackResult];
+        }
+        else if (fieldName != '' && node.nodeName.match(/INPUT|TEXTAREA/i)) {
+            fieldValue = getFieldValue(node, getDisabled);
+            if (null === fieldValue) {
+                result = [];
+            } else {
+                result = [ { name: fieldName, value: fieldValue} ];
+            }
+        }
+        else if (fieldName != '' && node.nodeName.match(/SELECT/i)) {
+	        fieldValue = getFieldValue(node, getDisabled);
+	        result = [ { name: fieldName.replace(/\[\]$/, ''), value: fieldValue } ];
+        }
+        else {
+            result = getSubFormValues(node, nodeCallback, useIdIfEmptyName, getDisabled);
+        }
+
+        return result;
+    }
+
+	function getFieldName(node, useIdIfEmptyName)
+	{
+		if (node.name && node.name != '') return node.name;
+		else if (useIdIfEmptyName && node.id && node.id != '') return node.id;
+		else return '';
+	}
+
+
+	function getFieldValue(fieldNode, getDisabled)
+	{
+		if (fieldNode.disabled && !getDisabled) return null;
+
+		switch (fieldNode.nodeName) {
+			case 'INPUT':
+			case 'TEXTAREA':
+				switch (fieldNode.type.toLowerCase()) {
+					case 'radio':
+			if (fieldNode.checked && fieldNode.value === "false") return false;
+					case 'checkbox':
+                        if (fieldNode.checked && fieldNode.value === "true") return true;
+                        if (!fieldNode.checked && fieldNode.value === "true") return false;
+			if (fieldNode.checked) return fieldNode.value;
+						break;
+
+					case 'button':
+					case 'reset':
+					case 'submit':
+					case 'image':
+						return '';
+						break;
+
+					default:
+						return fieldNode.value;
+						break;
+				}
+				break;
+
+			case 'SELECT':
+				return getSelectedOptionValue(fieldNode);
+				break;
+
+			default:
+				break;
+		}
+
+		return null;
+	}
+
+	function getSelectedOptionValue(selectNode)
+	{
+		var multiple = selectNode.multiple,
+			result = [],
+			options,
+			i, l;
+
+		if (!multiple) return selectNode.value;
+
+		for (options = selectNode.getElementsByTagName("option"), i = 0, l = options.length; i < l; i++)
+		{
+			if (options[i].selected) result.push(options[i].value);
+		}
+
+		return result;
+	}
+
+	return form2js;
+
+}));
diff --git a/_assets/templates/base.tmpl b/_assets/templates/base.tmpl
new file mode 100644
index 00000000..85b9a3ef
--- /dev/null
+++ b/_assets/templates/base.tmpl
@@ -0,0 +1,289 @@
+<!DOCTYPE html>
+<html>
+{{ $absURL := .Config.AbsoluteURL }}
+<head>
+    <title>{{.Name}}</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
+    <meta charset="utf-8">
+    <link rel="stylesheet" href="{{ .Config.AbsoluteURL }}/_internal/css/normalize.css">
+    <link rel="stylesheet" href="{{ .Config.AbsoluteURL }}/_internal/css/fonts.css">
+    <link rel="stylesheet" href="{{ .Config.AbsoluteURL }}/_internal/css/styles.css">
+    {{- if ne .User.StyleSheet "" -}}
+    <style>{{ CSS .User.StyleSheet }}</style>
+    {{- end -}}
+
+    <script>
+    var user = JSON.parse('{{ Marshal .User }}'),
+        webdavURL = "{{.Config.AbsoluteWebdavURL }}",
+        baseURL = "{{.Config.AbsoluteURL}}",
+        prefixURL = "{{ .Config.PrefixURL }}";
+    </script>
+    <script src="{{ .Config.AbsoluteURL }}/_internal/js/common.js" defer></script>
+    {{- if .IsDir }}
+    <script src="{{ .Config.AbsoluteURL }}/_internal/js/listing.js" defer></script>
+    {{- else }}
+    <script src="{{ .Config.AbsoluteURL }}/_internal/js/vendor/ace/src-min/ace.js" defer></script>
+    <script src="{{ .Config.AbsoluteURL }}/_internal/js/vendor/form2js.js" defer></script>
+    <script src="{{ .Config.AbsoluteURL }}/_internal/js/editor.js" defer></script>
+    {{- end }}
+</head>
+<body>
+    <header>
+        <div id="top-bar">
+            <div><p>File Manager</p></div>
+            <div id="search">
+                <i class="material-icons" title="Search">search</i>
+                <input type="text" aria-label="Write here to search" placeholder="Search or execute a command...">
+                <div>
+                    <div>Loading...</div>
+                    <p><i class="material-icons spin">autorenew</i></p>
+                </div>
+            </div>
+
+            <div class="action" id="logout" tabindex="0" role="button" aria-label="Log out">
+                <i class="material-icons" title="Logout">exit_to_app</i>
+            </div>
+        </div>
+
+        <div id="bottom-bar">
+            <div>
+                {{- if ne .Name "/"}}
+                <div data-dropdown tabindex="0" aria-label="Previous" role="button" class="action" id="previous">
+                    <i class="material-icons" title="Previous">subdirectory_arrow_left</i>
+                    <ul class="dropdown" id="breadcrumbs">
+                    {{- range $item := .BreadcrumbMap }}
+                        <a tabindex="0" href="{{ $absURL }}{{ $item.URL }}"><li>{{ $item.Name }}</li></a>
+                    {{- end }}
+                    </ul>
+                </div>
+                {{- end }}
+
+                {{ if ne .Name "/"}}<p id="current-file">{{ .Name }}</p>{{ end }}
+            </div>
+
+            <div class="actions{{ if .IsDir }} disabled{{ end }}" id="file-only">
+                {{- if and (not .IsDir) (.User.AllowEdit) }}
+                {{- if .Editor}}
+
+                {{- if eq .Data.Mode "markdown" }}
+                <div tabindex="0" role="button" aria-label="Preview" class="action" id="preview" onclick="notImplemented(event);">
+                    <i class="material-icons" title="Preview">remove_red_eye</i>
+                </div>
+                {{- end }}
+
+                {{- if eq .Data.Visual true }}
+                <div tabindex="0" role="button" aria-label="Toggle edit source" class="action" id="edit-source">
+                    <i class="material-icons" title="Toggle edit source">code</i>
+                </div>
+                {{- end }}
+                {{- end }}
+
+                <div tabindex="0" role="button" aria-label="Save" class="action" id="save">
+                    <i class="material-icons" title="Save">save</i>
+                </div>
+                {{- end }}
+
+                {{- if .IsDir }}
+                <div tabindex="0" role="button" aria-label="See raw" class="action" id="open">
+                    <i class="material-icons" title="See raw">open_in_new</i>
+                    <span>See raw</span>
+                </div>
+                {{- end }}
+
+                {{- if and (.User.AllowEdit) (.IsDir) }}
+                <div tabindex="0" role="button" aria-label="Move" class="action" id="move">
+                    <i class="material-icons" title="Move">forward</i>
+                    <span>Move file</span>
+                </div>
+                {{- end }}
+
+                {{- if and .IsDir .User.AllowEdit }}
+                <div tabindex="0" role="button" aria-label="Edit" class="action" id="rename">
+                    <i class="material-icons" title="Edit">mode_edit</i>
+                </div>
+                {{- end }}
+
+                {{- if and .User.AllowEdit .IsDir }}
+                <div tabindex="0" role="button" aria-label="Delete" class="action" id="delete">
+                    <i class="material-icons" title="Delete">delete</i><span>Delete</span>
+                </div>
+                {{- end }}
+            </div>
+
+            <div tabindex="0" role="button" aria-label="Moew" class="action mobile-only" id="more">
+                <i class="material-icons">more_vert</i>
+            </div>
+
+            <div class="actions" id="main-actions">
+                {{- if .IsDir }}
+                <div role="button" class="action" id="view">
+                    {{- if eq .Display "mosaic" }}
+                        <a tabindex="0" aria-label="Switch to list" title="Switch View" href="?display=list">
+                            <i class="material-icons">view_list</i><span>Switch view</span>
+                        </a>
+                    {{- else }}
+                        <a tabindex="0" aria-label="Switch to Mosaic" title="Switch View" href="?display=mosaic">
+                            <i class="material-icons">view_module</i><span>Switch view</span>
+                        </a>
+                    {{- end }}
+                </div>
+
+                <div tabindex="0" role="button" aria-label="Select multiple" class="action mobile-only" id="multiple-selection-activate">
+                    <i class="material-icons">check_circle</i><span>Select</span>
+                </div>
+                {{- end }}
+
+                {{- if and (.User.AllowNew) (.IsDir) }}
+                <div tabindex="0" aria-label="Upload" role="button" class="action" id="upload">
+                    <i class="material-icons" title="Upload">file_upload</i><span>Upload</span>
+                </div>
+                {{- end }}
+
+                {{- if not .IsDir }}
+                <div tabindex="0" role="button" aria-label="See raw" class="action" id="open">
+                    <i class="material-icons" title="See raw">open_in_new</i>
+                    <span>See raw</span>
+                </div>
+                {{- end }}
+
+                {{- if and .User.AllowEdit (not .IsDir) }}
+                <div tabindex="0" role="button" aria-label="Delete" class="action" id="delete">
+                    <i class="material-icons" title="Delete">delete</i><span>Delete</span>
+                </div>
+                {{- end }}
+
+                <div {{ if .IsDir }}data-dropdown{{ end }} tabindex="0" role="button" aria-label="Download" class="action" id="download">
+                    {{- if not .IsDir}}<a href="?download=true">{{ end }}
+                    <i class="material-icons" title="Download">file_download</i><span>Download</span>
+                    {{- if not .IsDir}}</a>{{ end }}
+
+                    {{- if .IsDir }}
+                    <ul class="dropdown" id="download-drop">
+                        <a tabindex="0" aria-label="Download as Zip" data-format="zip" href="?download=zip"><li>zip</li></a>
+                        <a tabindex="0" aria-label="Download as Tar" data-format="tar" href="?download=tar"><li>tar</li></a>
+                        <a tabindex="0" aria-label="Download as TarGz" data-format="targz" href="?download=targz"><li>tar.gz</li></a>
+                        <a tabindex="0" aria-label="Download as TarBz2" data-format="tarbz2" href="?download=tarbz2"><li>tar.bz2</li></a>
+                        <a tabindex="0" aria-label="Download as TarXz" data-format="tarbz2" href="?download=tarxz"><li>tar.xz</li></a>
+                    </ul>
+                    {{- end }}
+                </div>
+
+                <div tabindex="0" role="button" aria-label="Info" class="action" id="info">
+                    <i class="material-icons" title="Info">info</i><span>Info</span>
+                </div>
+            </div>
+        </div>
+
+        <div id="click-overlay"></div>
+    </header>
+
+    <div id="multiple-selection" class="mobile-only">
+        <p>Multiple selection enabled</p>
+        <div tabindex="0" role="button" class="action" id="multiple-selection-cancel">
+            <i class="material-icons" title="Clear">clear</i>
+        </div>
+    </div>
+
+    <main>
+        {{- template "content" . }}
+    </main>
+
+    <div class="overlay"></div>
+
+    {{- if and (.User.AllowNew) (.IsDir) }}
+    <div class="floating">
+        <div tabindex="0" role="button" class="action" id="new">
+            <i class="material-icons" title="New file or directory">add</i>
+        </div>
+    </div>
+    {{- end }}
+
+    <template id="question-template">
+        <form class="prompt">
+            <h3></h3>
+            <p></p>
+            <input autofocus type="text">
+            <div>
+                <button type="submit" autofocus class="ok">OK</button>
+                <button class="cancel" onclick="closePrompt(event);">Cancel</button>
+            </div>
+        </form>
+    </template>
+
+    <template id="info-template">
+        <div class="prompt">
+            <h3>File Information</h3>
+            <p><strong>Display Name:</strong> <span id="display_name"></span></p>
+            <p><strong>Content Length:</strong> <span id="content_length"></span> Bytes</p>
+            <p><strong>Last Modified:</strong> <span id="last_modified"></span></p>
+
+            <section class="file-only">
+                <p><strong>MD5:</strong> <code id="md5"><a href="#" onclick="getHash(event, 'md5')">show</a></code></p>
+                <p><strong>SHA1:</strong> <code id="sha1"><a href="#" onclick="getHash(event, 'sha1')">show</a></code></p>
+                <p><strong>SHA256:</strong> <code id="sha256"><a href="#" onclick="getHash(event, 'sha256')">show</a></code></p>
+                <p><strong>SHA512:</strong> <code id="sha512"><a href="#" onclick="getHash(event, 'sha512')">show</a></code></p>
+            </section>
+
+            <div>
+                <button type="submit" onclick="closePrompt(event);" class="ok">OK</button>
+            </div>
+        </div>
+    </template>
+
+    <template id="message-template">
+        <div class="prompt">
+            <h3></h3>
+            <p></p>
+            <div>
+                <button type="submit" onclick="closePrompt(event);" class="ok">OK</button>
+            </div>
+        </div>
+    </template>
+
+    <template id="move-template">
+        <form class="prompt">
+            <h3>Move</h3>
+            <p>Choose new house for your file(s)/folder(s):</p>
+
+            <div class="file-list">
+                <ul>
+                </ul>
+            </div>
+
+            <p>Currently navigating on: <code></code>.</p>
+
+            <div>
+                <button type="submit" autofocus class="ok">Move</button>
+                <button class="cancel" onclick="closePrompt(event);">Cancel</button>
+            </div>
+        </form>
+    </template>
+
+    <div class="help">
+        <h3>Help</h3>
+
+        <ul>
+            <li><strong>F1</strong> - this information</li>
+            <li><strong>F2</strong> - rename file</li>
+            <li><strong>DEL</strong> - delete selected items</li>
+            <li><strong>ESC</strong> - clear selection and/or close the prompt</li>
+            <li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
+            <li><strong>CTRL + Click</strong> - select multiple files or directories</li>
+            <li><strong>Double click</strong> - open a file or directory</li>
+            <li><strong>Click</strong> - select file or directory</li>
+        </ul>
+
+        <p>Not available yet</p>
+
+        <ul>
+            <li><strong>Alt + Click</strong> - select a group of files</li>
+        </ul>
+
+        <div>
+            <button type="submit" onclick="closeHelp(event);" class="ok">OK</button>
+        </div>
+    </div>
+
+    <footer>Served with <a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a> and <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.</footer>
+</body>
+</html>
diff --git a/_assets/templates/editor.tmpl b/_assets/templates/editor.tmpl
new file mode 100644
index 00000000..d02238d8
--- /dev/null
+++ b/_assets/templates/editor.tmpl
@@ -0,0 +1,57 @@
+{{ define "content" }}
+{{- with .Data }}
+<form id="editor" {{ if eq .Mode "markdown" }}class="markdown"{{ end }} data-kind="{{ .Class }}" data-rune="{{ if eq .Class "complete" }}{{ .FrontMatter.Rune }}{{ end }}">
+    {{- if or (eq .Class "frontmatter-only") (eq .Class "complete") }}
+    {{- if (eq .Class "complete")}}
+    <h2>Metadata</h2>
+    {{- end }}
+    <div class="frontmatter" data-type="parent">
+        {{- template "blocks" .FrontMatter.Content }}
+        <div class="button add">Add field</div>
+    </div>
+    {{- end }}
+
+    {{ if or (eq .Class "content-only") (eq .Class "complete") }}
+        {{ if (eq .Class "complete")}}
+        <h2>Body</h2>
+        {{ end }}
+        <div class="content">
+            <div id="ace" data-mode="{{ .Mode }}"></div>
+            <textarea class="source" name="content">{{ .Content }}</textarea>
+        </div>
+    {{ end }}
+</form>
+{{- end }}
+
+<template id="base-template">
+    <fieldset id="" data-type="">
+        <h3></h3>
+        <div class="action add">
+            <i class="material-icons">add</i>
+        </div>
+        <div class="action delete" data-delete="">
+            <i class="material-icons">close</i>
+        </div>
+        <div class="group"></div>
+    </fieldset>
+</template>
+
+<template id="object-item-template">
+    <div class="block" id="block-${bid}" data-content="${bid}">
+        <label for="${bid}">${name}</label>
+        <input name="${bid}" id="${bid}" type="text" data-parent-type="object"></input>
+        <div class="action delete" data-delete="block-${bid}">
+            <i class="material-icons">close</i>
+        </div>
+    </div>
+</template>
+
+<template id="array-item-template">
+    <div id="" data-type="array-item">
+        <input name="" id="" type="text" data-parent-type="array"></input>
+        <div class="action delete" data-delete="">
+            <i class="material-icons">close</i>
+        </div>
+    </div>
+</template>
+{{ end }}
diff --git a/_assets/templates/frontmatter.tmpl b/_assets/templates/frontmatter.tmpl
new file mode 100644
index 00000000..3389da90
--- /dev/null
+++ b/_assets/templates/frontmatter.tmpl
@@ -0,0 +1,56 @@
+{{ define "blocks" }}
+{{ if .Fields }}<div class="group">{{ end }}
+{{- range $key, $value := .Fields }}
+    {{- if eq $value.Parent.Type "array" }}
+        <div id="{{ $value.Name }}-{{ $key }}" data-type="array-item">
+            {{- template "value" $value }}
+            <div class="action delete" data-delete="{{ $value.Name }}-{{ $key }}">
+                <i class="material-icons" title="Close">close</i>
+            </div>
+        </div>
+    {{- else }}
+        <div class="block" id="block-{{ $value.Name }}" data-content="{{ $value.Name }}">
+            <label for="{{ $value.Name }}">{{ $value.Title }}</label>
+            {{ template "value" $value }}
+            <div class="action delete" data-delete="block-{{ $value.Name }}">
+                <i class="material-icons" title="Close">close</i>
+            </div>
+        </div>
+    {{- end }}
+{{- end }}
+{{- if .Fields }}</div>{{ end }}
+
+{{- range $key, $value := .Arrays }}
+{{- template "fielset" $value }}
+{{- end }}
+
+{{- range $key, $value := .Objects }}
+{{- template "fielset" $value }}
+{{- end }}
+
+{{ end }}
+
+{{ define "value" }}
+{{- if eq .HTMLType "textarea" }}
+    <textarea class="scroll" name="{{ .Name }}" id="{{.Name }}" data-parent-type="{{ .Parent.Type }}">{{ .Content.Other }}</textarea>
+{{- else if eq .HTMLType "datetime" }}
+    <input name="{{ .Name }}" id="{{ .Name }}" value="{{ .Content.Other.Format "2006-01-02T15:04" }}" type="datetime-local" data-parent-type="{{ .Parent.Type }}"></input>
+{{- else }}
+    <input name="{{ .Name }}" id="{{ .Name }}" value="{{ .Content.Other }}" type="{{ .HTMLType }}" data-parent-type="{{ .Parent.Type }}"></input>
+{{- end }}
+{{ end }}
+
+{{ define "fielset" }}
+<fieldset id="{{ .Name }}" data-type="{{ .Type }}">
+    {{- if not (eq .Title "") }}
+        <h3>{{ .Name }}</h3>
+    {{- end }}
+    <div class="action add">
+        <i class="material-icons" title="Add">add</i>
+    </div>
+    <div class="action delete" data-delete="{{ .Name }}">
+        <i class="material-icons" title="Close">close</i>
+    </div>
+    {{- template "blocks" .Content }}
+</fieldset>
+{{ end }}
diff --git a/_assets/templates/listing.tmpl b/_assets/templates/listing.tmpl
new file mode 100644
index 00000000..da94b962
--- /dev/null
+++ b/_assets/templates/listing.tmpl
@@ -0,0 +1,103 @@
+{{ define "content" }}
+<div class="container {{ .Display }}" id="listing">
+{{- with .Data -}}
+    <div>
+        <div class="item header">
+            <div></div>
+            <div>
+                <p class="name{{ if eq .Sort "name" }} active{{ end }}"><span>Name</span>
+                    {{- if eq .Sort "name" -}}
+                    {{- if eq .Order "asc" -}}
+                    <a href="?sort=name&order=desc"><i class="material-icons">arrow_downward</i></a>
+                    {{- else -}}
+                    <a href="?sort=name&order=asc"><i class="material-icons">arrow_upward</i></a>
+                    {{- end -}}
+                    {{- else -}}
+                    <a href="?sort=name&order=desc"><i class="material-icons">arrow_downward</i></a>
+                    {{- end -}}
+                </p>
+                <p class="size{{ if eq .Sort "size" }} active{{ end }}"><span>File Size</span>
+                    {{- if eq .Sort "size" -}}
+                    {{- if eq .Order "asc" -}}
+                    <a href="?sort=size&order=desc"><i class="material-icons">arrow_downward</i></a>
+                    {{- else -}}
+                    <a href="?sort=size&order=asc"><i class="material-icons">arrow_upward</i></a>
+                    {{- end -}}
+                    {{- else -}}
+                    <a href="?sort=size&order=desc"><i class="material-icons">arrow_downward</i></a>
+                    {{- end -}}
+                </p>
+                <p class="modified">Last modified</p>
+            </div>
+        </div>
+    </div>
+    
+    {{ if and (eq .NumDirs 0) (eq .NumFiles 0) }}
+    <h2 class="message">It feels lonely here :'(</h2>
+    {{ end }}
+    
+    {{- if not (eq .NumDirs 0)}}
+    <h2>Folders</h2>
+    <div>
+    {{- range .Items }}
+        {{- if (.IsDir) }}
+        {{ template "item" .}}
+        {{- end }}
+    {{- end }}
+    </div>
+    {{- end }}
+        
+    {{- if not (eq .NumFiles 0)}}
+    <h2>Files</h2>
+    <div>
+    {{- range .Items }}
+        {{- if (not .IsDir) }}
+        {{ template "item" .}}
+        {{- end }}
+    {{- end }}
+    </div>
+    {{- end }}
+</div>
+
+<input style="display:none" type="file" id="upload-input" onchange="listing.handleFiles(this.files, '')" value="Upload" multiple>
+{{- end -}}
+{{- end -}}
+
+{{ define "item" }}
+<div ondragstart="listing.itemDragStart(event)" 
+    {{ if .IsDir}}ondragover="listing.itemDragOver(event)" ondrop="listing.itemDrop(event)"{{ end }} 
+    draggable="true" 
+    class="item" 
+    onclick="listing.selectItem(event)"
+    ondblclick="listing.openItem(event)"
+    data-dir="{{ .IsDir }}" 
+    data-url="{{ .URL }}"
+    id="{{ EncodeBase64 .Name }}">
+    <div>
+        {{- if .IsDir}}
+        <i class="material-icons">folder</i>
+        {{- else}}
+        {{ if eq .Type "image" }}
+        <i class="material-icons">insert_photo</i>
+        {{ else if eq .Type "audio" }}
+        <i class="material-icons">volume_up</i>
+        {{ else if eq .Type "video" }}
+        <i class="material-icons">movie</i>
+        {{ else }}
+        <i class="material-icons">insert_drive_file</i>
+        {{ end }}
+        {{- end}}
+    </div>
+    <div>
+        <p class="name">{{.Name}}</p>
+        {{- if .IsDir}}
+        <p class="size" data-order="-1">&mdash;</p>
+        {{- else}}
+        <p class="size" data-order="{{.Size}}">{{.HumanSize}}</p>
+        {{- end}}
+        <p class="modified">
+        <time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "2 Jan 2006 03:04 PM"}}</time>
+        </p>
+    </div>
+</div>
+{{ end }}
diff --git a/_assets/templates/minimal.tmpl b/_assets/templates/minimal.tmpl
new file mode 100644
index 00000000..66e0068d
--- /dev/null
+++ b/_assets/templates/minimal.tmpl
@@ -0,0 +1 @@
+{{ template "content" . }}
diff --git a/_assets/templates/single.tmpl b/_assets/templates/single.tmpl
new file mode 100644
index 00000000..c85ab5a4
--- /dev/null
+++ b/_assets/templates/single.tmpl
@@ -0,0 +1,23 @@
+{{ define "content" }}
+{{ with .Data}}
+<main class="container">
+  {{ if eq .Type "image" }}
+  <center><img src="{{ .URL }}?raw=true"></center>
+  {{ else if eq .Type "audio" }}
+  <audio src="{{ .URL }}?raw=true" controls></audio>
+  {{ else if eq .Type "video" }}
+  <video src="{{ .URL }}?raw=true" controls>
+    Sorry, your browser doesn't support embedded videos,
+    but don't worry, you can <a href="?download=true">download it</a>
+    and watch it with your favorite video player!
+  </video>
+  {{ else if eq .Extension ".pdf" }}
+  <object class="pdf" data="{{ .URL }}?raw=true"></object>
+  {{ else if eq .Type "blob" }}
+  <a href="?download=true"><h2 class="message">Download <i class="material-icons">file_download</i></h2></a>
+  {{ else}}
+  <pre>{{ .StringifyContent }}</pre>
+  {{ end }}
+</main>
+{{ end }}
+{{ end }}
diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go
new file mode 100644
index 00000000..bb0f3407
--- /dev/null
+++ b/cmd/filemanager/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+	"log"
+	"net/http"
+
+	"github.com/hacdias/filemanager"
+)
+
+
+var m *filemanager.FileManager
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	_, err := m.ServeHTTP(w, r)
+	if err != nil {
+		log.Print(err)
+	}
+}
+
+func main() {
+	m = filemanager.New()
+	http.HandleFunc("/", handler)
+	http.ListenAndServe(":8080", nil)
+}
\ No newline at end of file
diff --git a/filemanager.go b/filemanager.go
index f65c3362..022bc9ab 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -49,10 +49,10 @@ type User struct {
 
 // Assets are the static and front-end assets, such as JS, CSS and HTML templates.
 type Assets struct {
-	requiredJS rice.Box // JS that is always required to have in order to be usable.
-	Templates  rice.Box
-	CSS        rice.Box
-	JS         rice.Box
+	requiredJS *rice.Box // JS that is always required to have in order to be usable.
+	Templates  *rice.Box
+	CSS        *rice.Box
+	JS         *rice.Box
 }
 
 // Rule is a dissalow/allow rule.
@@ -66,16 +66,65 @@ type Rule struct {
 // CommandFunc ...
 type CommandFunc func(r *http.Request, c *FileManager, u *User) error
 
-// AbsoluteURL ...
+func New() *FileManager {
+	m := &FileManager{
+		User: &User{
+			AllowCommands: true,
+			AllowEdit:     true,
+			AllowNew:      true,
+			Commands:      []string{"git", "svn", "hg"},
+			Rules: []*Rule{{
+				Regex:  true,
+				Allow:  false,
+				Regexp: regexp.MustCompile("\\/\\..+"),
+			}},
+		},
+		Users:      map[string]*User{},
+		BeforeSave: func(r *http.Request, c *FileManager, u *User) error { return nil },
+		AfterSave:  func(r *http.Request, c *FileManager, u *User) error { return nil },
+		Assets: &Assets{
+			Templates:  rice.MustFindBox("./_assets/templates"),
+			CSS:        rice.MustFindBox("./_assets/css"),
+			requiredJS: rice.MustFindBox("./_assets/js"),
+		},
+	}
+
+	m.SetScope(".")
+	m.SetBaseURL("/")
+	m.SetWebDavURL("/webdav")
+
+	return m
+}
+
 func (m FileManager) AbsoluteURL() string {
 	return m.PrefixURL + m.BaseURL
 }
 
-// AbsoluteWebdavURL ...
 func (m FileManager) AbsoluteWebdavURL() string {
 	return m.PrefixURL + m.WebDavURL
 }
 
+func (m *FileManager) SetBaseURL(url string) {
+	url = strings.TrimPrefix(url, "/")
+	url = strings.TrimSuffix(url, "/")
+	url = "/" + url
+	m.BaseURL = strings.TrimSuffix(url, "/")
+}
+
+func (m *FileManager) SetWebDavURL(url string) {
+	m.WebDavURL = m.BaseURL + "/" + strings.TrimPrefix(url, "/")
+	m.User.Handler = &webdav.Handler{
+		Prefix:     m.WebDavURL,
+		FileSystem: m.FileSystem,
+		LockSystem: webdav.NewMemLS(),
+	}
+}
+
+func (u *User) SetScope(scope string) {
+	u.Scope = strings.TrimSuffix(scope, "/")
+	u.FileSystem = webdav.Dir(u.Scope)
+}
+
 // Allowed checks if the user has permission to access a directory/file.
 func (u User) Allowed(url string) bool {
 	var rule *Rule
diff --git a/http.go b/http.go
index cfe4b852..faa6c140 100644
--- a/http.go
+++ b/http.go
@@ -6,10 +6,15 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-
-	"github.com/mholt/caddy/caddyhttp/httpserver"
 )
 
+func matchURL(first, second string) bool {
+	first = strings.ToLower(first)
+	second = strings.ToLower(second)
+
+	return strings.HasPrefix(first, second)
+}
+
 // ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
 func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
 	var (
@@ -21,7 +26,7 @@ func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
 
 	// Checks if the URL matches the Assets URL. Returns the asset if the
 	// method is GET and Status Forbidden otherwise.
-	if httpserver.Path(r.URL.Path).Matches(c.BaseURL + AssetsURL) {
+	if matchURL(r.URL.Path, c.BaseURL+AssetsURL) {
 		if r.Method == http.MethodGet {
 			return serveAssets(w, r, c)
 		}
@@ -37,7 +42,7 @@ func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
 	}
 
 	// Checks if the request URL is for the WebDav server
-	if httpserver.Path(r.URL.Path).Matches(c.WebDavURL) {
+	if matchURL(r.URL.Path, c.WebDavURL) {
 		// Checks for user permissions relatively to this PATH
 		if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
 			return http.StatusForbidden, nil
diff --git a/http_assets.go b/http_assets.go
index b15513d6..8cf5e564 100644
--- a/http_assets.go
+++ b/http_assets.go
@@ -9,7 +9,7 @@ import (
 )
 
 // AssetsURL is the url of the assets
-const AssetsURL = "/_filemanagerinternal"
+const AssetsURL = "/_internal"
 
 // Serve provides the needed assets for the front-end
 func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, error) {
@@ -27,8 +27,13 @@ func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, e
 		filename = strings.Replace(filename, "/js/", "", 1)
 		file, err = m.Assets.requiredJS.Bytes(filename)
 	case strings.HasPrefix(filename, "/vendor"):
-		filename = strings.Replace(filename, "/vendor/", "", 1)
-		file, err = m.Assets.JS.Bytes(filename)
+		if m.Assets.JS != nil {
+			filename = strings.Replace(filename, "/vendor/", "", 1)
+			file, err = m.Assets.JS.Bytes(filename)
+			break
+		}
+
+		fallthrough
 	default:
 		err = errors.New("not found")
 	}
diff --git a/info.go b/info.go
index 741f03bd..b828476c 100644
--- a/info.go
+++ b/info.go
@@ -77,7 +77,7 @@ var textExtensions = [...]string{
 
 // RetrieveFileType obtains the mimetype and a simplified internal Type
 // using the first 512 bytes from the file.
-func (i FileInfo) RetrieveFileType() error {
+func (i *FileInfo) RetrieveFileType() error {
 	i.Mimetype = mime.TypeByExtension(i.Extension)
 
 	if i.Mimetype == "" {
@@ -128,7 +128,7 @@ func (i FileInfo) RetrieveFileType() error {
 }
 
 // Reads the file.
-func (i FileInfo) Read() error {
+func (i *FileInfo) Read() error {
 	if len(i.content) != 0 {
 		return nil
 	}