add editor and frontmatter thing

pull/144/head
Henrique Dias 2016-06-23 23:21:44 +01:00
parent 8b7c83539a
commit ddc6839d24
13 changed files with 897 additions and 282 deletions

View File

@ -8,12 +8,12 @@
*/
html {
font-family: sans-serif;
/* 1 */
-ms-text-size-adjust: 100%;
/* 2 */
-webkit-text-size-adjust: 100%;
/* 2 */
font-family: sans-serif;
/* 1 */
-ms-text-size-adjust: 100%;
/* 2 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/**
@ -21,7 +21,7 @@ html {
*/
body {
margin: 0;
margin: 0;
}
/* HTML5 display definitions
@ -36,7 +36,7 @@ body {
*/
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
display: block;
display: block;
}
/**
@ -45,10 +45,10 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu,
*/
audio, canvas, progress, video {
display: inline-block;
/* 1 */
vertical-align: baseline;
/* 2 */
display: inline-block;
/* 1 */
vertical-align: baseline;
/* 2 */
}
/**
@ -57,8 +57,8 @@ audio, canvas, progress, video {
*/
audio:not([controls]) {
display: none;
height: 0;
display: none;
height: 0;
}
/**
@ -67,7 +67,7 @@ audio:not([controls]) {
*/
[hidden], template {
display: none;
display: none;
}
/* Links
@ -79,7 +79,7 @@ audio:not([controls]) {
*/
a {
background-color: transparent;
background-color: transparent;
}
/**
@ -88,7 +88,7 @@ a {
*/
a:active, a:hover {
outline: 0;
outline: 0;
}
/* Text-level semantics
@ -100,7 +100,7 @@ a:active, a:hover {
*/
abbr[title] {
border-bottom: 1px dotted;
border-bottom: 1px dotted;
}
/**
@ -108,7 +108,7 @@ abbr[title] {
*/
b, strong {
font-weight: 500;
font-weight: 500;
}
/**
@ -116,7 +116,7 @@ b, strong {
*/
dfn {
font-style: italic;
font-style: italic;
}
/**
@ -125,8 +125,8 @@ dfn {
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
font-size: 2em;
margin: 0.67em 0;
}
/**
@ -134,8 +134,8 @@ h1 {
*/
mark {
background: #ff0;
color: #000;
color: #000;
background: #ff0;
}
/**
@ -143,7 +143,7 @@ mark {
*/
small {
font-size: 80%;
font-size: 80%;
}
/**
@ -151,16 +151,16 @@ small {
*/
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
top: -.5em;
}
sub {
bottom: -0.25em;
bottom: -.25em;
}
/* Embedded content
@ -172,8 +172,8 @@ sub {
*/
img {
border: 0;
max-width: 100%;
max-width: 100%;
border: 0;
}
/**
@ -181,7 +181,7 @@ img {
*/
svg:not(:root) {
overflow: hidden;
overflow: hidden;
}
/* Grouping content
@ -193,7 +193,7 @@ svg:not(:root) {
*/
figure {
margin: 1em 40px;
margin: 1em 40px;
}
/**
@ -201,8 +201,8 @@ figure {
*/
hr {
box-sizing: content-box;
height: 0;
box-sizing: content-box;
height: 0;
}
/**
@ -210,7 +210,7 @@ hr {
*/
pre {
overflow: auto;
overflow: auto;
}
/**
@ -218,8 +218,8 @@ pre {
*/
code, kbd, pre, samp {
font-family: monospace, monospace;
font-size: 1em;
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
@ -240,12 +240,12 @@ code, kbd, pre, samp {
*/
button, input, optgroup, select, textarea {
color: inherit;
/* 1 */
font: inherit;
/* 2 */
margin: 0;
/* 3 */
/* 1 */
font: inherit;
/* 2 */
margin: 0;
color: inherit;
/* 3 */
}
/**
@ -253,7 +253,7 @@ button, input, optgroup, select, textarea {
*/
button {
overflow: visible;
overflow: visible;
}
/**
@ -264,7 +264,7 @@ button {
*/
button, select {
text-transform: none;
text-transform: none;
}
/**
@ -275,14 +275,14 @@ button, select {
* `input` and others.
*/
button, html input[type="button"],
button, html input[type='button'],
/* 1 */
input[type="reset"], input[type="submit"] {
-webkit-appearance: button;
/* 2 */
cursor: pointer;
/* 3 */
input[type='reset'], input[type='submit'] {
/* 2 */
cursor: pointer;
-webkit-appearance: button;
/* 3 */
}
/**
@ -290,7 +290,7 @@ input[type="reset"], input[type="submit"] {
*/
button[disabled], html input[disabled] {
cursor: default;
cursor: default;
}
/**
@ -298,8 +298,8 @@ button[disabled], html input[disabled] {
*/
button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
padding: 0;
padding: 0;
border: 0;
}
/**
@ -308,7 +308,7 @@ button::-moz-focus-inner, input::-moz-focus-inner {
*/
input {
line-height: normal;
line-height: normal;
}
/**
@ -319,11 +319,11 @@ input {
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"], input[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
input[type='checkbox'], input[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
@ -332,8 +332,8 @@ input[type="checkbox"], input[type="radio"] {
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button {
height: auto;
input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
@ -341,11 +341,11 @@ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-o
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
*/
input[type="search"] {
-webkit-appearance: textfield;
/* 1 */
box-sizing: content-box;
/* 2 */
input[type='search'] {
/* 1 */
box-sizing: content-box;
-webkit-appearance: textfield;
/* 2 */
}
/**
@ -354,8 +354,8 @@ input[type="search"] {
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
input[type='search']::-webkit-search-cancel-button, input[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
@ -363,9 +363,9 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
border: 1px solid #c0c0c0;
}
/**
@ -374,10 +374,10 @@ fieldset {
*/
legend {
border: 0;
/* 1 */
padding: 0;
/* 2 */
/* 1 */
padding: 0;
border: 0;
/* 2 */
}
/**
@ -385,7 +385,7 @@ legend {
*/
textarea {
overflow: auto;
overflow: auto;
}
/**
@ -394,7 +394,7 @@ textarea {
*/
optgroup {
font-weight: bold;
font-weight: bold;
}
/* Tables
@ -406,342 +406,365 @@ optgroup {
*/
table {
border-collapse: collapse;
border-spacing: 0;
border-spacing: 0;
border-collapse: collapse;
}
td, th {
padding: 0;
padding: 0;
}
/* TANANANA */
body {
font-family: 'Roboto', sans-serif;
text-rendering: optimizespeed;
padding-top: 5em;
background-color: #fcfcfc;
font-family: 'Roboto', sans-serif;
padding-top: 5em;
background-color: #fcfcfc;
text-rendering: optimizespeed;
}
a {
color: #006ed3;
text-decoration: none;
text-decoration: none;
color: #006ed3;
}
a:hover, h1 a:hover {
color: #319cff;
color: #319cff;
}
header, #summary {
padding-left: 7%;
padding-right: 7%;
#summary, header {
padding-right: 7%;
padding-left: 7%;
}
th:first-child, td:first-child {
padding-left: 1em;
td:first-child, th:first-child {
padding-left: 1em;
}
th:last-child, td:last-child {
padding-right: 1em;
td:last-child, th:last-child {
padding-right: 1em;
}
h1 {
font-size: 1.5em;
font-weight: normal;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
font-size: 1.5em;
font-weight: normal;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
h1 a {
color: inherit;
color: inherit;
}
h1 a:hover {
text-decoration: underline;
text-decoration: underline;
}
main {
display: block;
display: block;
}
.meta-item {
margin-right: 1em;
margin-right: 1em;
}
table {
width: 100%;
border-collapse: collapse;
width: 100%;
border-collapse: collapse;
}
tr {
border-bottom: 1px dashed #dadada;
transition: .1s ease all;
cursor: pointer;
cursor: pointer;
transition: 0.1s ease all;
border-bottom: 1px dashed #dadada;
}
tr.selected {
background-color: #ccc;
background-color: #ccc;
}
th, td {
text-align: left;
padding: 1em 0;
td, th {
padding: 1em 0;
text-align: left;
}
th {
padding-top: 15px;
padding-bottom: 15px;
font-size: 16px;
white-space: nowrap;
font-size: 16px;
padding-top: 15px;
padding-bottom: 15px;
white-space: nowrap;
}
th a {
color: black;
color: black;
}
th svg {
vertical-align: middle;
vertical-align: middle;
}
td {
font-size: 14px;
font-size: 14px;
}
td:first-child {
width: 50%;
width: 50%;
}
th:last-child, td:last-child {
text-align: right;
td:last-child, th:last-child {
text-align: right;
}
td:first-child svg {
position: absolute;
position: absolute;
}
td .name, td .goup {
margin-left: 1.1em;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
color: #424242;
vertical-align: middle;
td .goup, td .name {
margin-left: 1.1em;
vertical-align: middle;
white-space: pre-wrap;
word-break: break-all;
color: #424242;
overflow-wrap: break-word;
}
footer {
font-size: .6em;
text-align: center;
color: grey;
margin: 1em 0;
font-size: 0.6em;
margin: 1em 0;
text-align: center;
color: grey;
}
footer a,
footer a:hover {
color: inherit;
footer a, footer a:hover {
color: inherit;
}
.container {
margin: 0 auto;
width: 95%;
max-width: 960px;
width: 95%;
max-width: 960px;
margin: 0 auto;
}
pre {
border: 1px solid #e6e6e6;
background-color: #f5f5f5;
border-radius: .5em;
padding: 1em;
padding: 1em;
border: 1px solid #e6e6e6;
border-radius: 0.5em;
background-color: #f5f5f5;
}
@media (max-width: 600px) {
.hideable {
display: none;
}
td:first-child {
width: auto;
}
th:nth-child(2), td:nth-child(2) {
padding-right: 5%;
text-align: right;
}
.hideable {
display: none;
}
td:first-child {
width: auto;
}
td:nth-child(2), th:nth-child(2) {
padding-right: 5%;
text-align: right;
}
}
/* MY STYLES */
* {
box-sizing: border-box;
box-sizing: border-box;
}
/* MATERIAL ICONS */
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 1.5em;
/* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
font-family: 'Material Icons';
font-size: 1.5em;
font-weight: normal;
font-style: normal;
line-height: 1;
/* Preferred icon size */
display: inline-block;
white-space: nowrap;
letter-spacing: normal;
text-transform: none;
word-wrap: normal;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
/* HEADER */
header {
background-color: #2196f3;
padding: 1.7em 0;
z-index: 999;
z-index: 999;
padding: 1.7em 0;
background-color: #2196f3;
}
header h1 {
margin: 0;
font-size: 2em;
font-size: 2em;
margin: 0;
}
header a,
header a:hover {
color: inherit;
header a, header a:hover {
color: inherit;
}
header p {
font-size: 1.5em;
font-size: 1.5em;
}
header p i {
font-size: 1em !important;
color: rgba(255, 255, 255, 0.31);
font-size: 1em !important;
color: rgba(255, 255, 255, .31);
}
header p i {
vertical-align: middle;
vertical-align: middle;
}
header form {
display: inline-block;
background-color: #1E88E5;
padding: .75em;
color: #fff;
border-radius: .3em;
height: 100%;
vertical-align: middle;
display: inline-block;
height: 100%;
padding: 0.75em;
vertical-align: middle;
color: #fff;
border-radius: 0.3em;
background-color: #1e88e5;
}
header form input, header form i {
vertical-align: middle;
header form i, header form input {
vertical-align: middle;
}
header form i {
margin-right: .3em;
color: rgba(255,255,255, 0.5)
margin-right: 0.3em;
color: rgba(255, 255, 255, .5);
}
header form input {
border: 0;
outline: 0;
background-color: transparent;
min-width: 20em;
min-width: 20em;
border: 0;
outline: 0;
background-color: transparent;
}
::-webkit-input-placeholder {
/* WebKit, Blink, Edge */
color: rgba(255,255,255, 0.5);
/* WebKit, Blink, Edge */
color: rgba(255, 255, 255, .5);
}
:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: rgba(255,255,255, 0.5);
opacity: 1;
opacity: 1;
/* Mozilla Firefox 4 to 18 */
color: rgba(255, 255, 255, .5);
}
::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: rgba(255,255,255, 0.5);
opacity: 1;
opacity: 1;
/* Mozilla Firefox 19+ */
color: rgba(255, 255, 255, .5);
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: rgba(255,255,255, 0.5);
/* Internet Explorer 10-11 */
color: rgba(255, 255, 255, .5);
}
header, #toolbar {
position: fixed;
width: 100%;
top: 0;
left: 0;
padding: .5em;
display: flex;
color: #fff;
#toolbar, header {
position: fixed;
top: 0;
left: 0;
display: flex;
width: 100%;
padding: 0.5em;
color: #fff;
}
#toolbar div, header div {
flex-grow: 1;
vertical-align: middle;
vertical-align: middle;
flex-grow: 1;
}
#toolbar p, header p {
display: inline-block;
margin: 0;
vertical-align: middle;
display: inline-block;
margin: 0;
vertical-align: middle;
}
#toolbar p a, header p a,
#toolbar p a:hover, header p a:hover {
color: inherit;
#toolbar p a, #toolbar p a:hover, header p a, header p a:hover {
color: inherit;
}
#toolbar {
background-color: #6f6f6f;
color: #fff;
top: -4em;
opacity: 0;
transition: .2s ease-in-out all;
z-index: 1000;
z-index: 1000;
top: -4em;
transition: 0.2s ease-in-out all;
opacity: 0;
color: #fff;
background-color: #6f6f6f;
}
#toolbar.enabled {
top: 0;
opacity: 1;
top: 0;
opacity: 1;
}
#toolbar div:nth-child(2), header div:nth-child(2) {
text-align: right;
text-align: right;
}
.action {
border: 0;
border-radius: 50%;
margin: 0 .2em;
display: inline-block;
cursor: pointer;
transition: .2s ease all;
display: inline-block;
margin: 0 0.2em;
cursor: pointer;
transition: 0.2s ease all;
border: 0;
border-radius: 50%;
}
.action.disabled {
opacity: .2;
opacity: 0.2;
}
.action i {
padding: .5em;
border-radius: 50%;
transition: .2s ease-in-out all;
padding: 0.5em;
transition: 0.2s ease-in-out all;
border-radius: 50%;
}
.action:hover i {
background-color: rgba(0, 0, 0, 0.1);
background-color: rgba(0, 0, 0, .1);
}
/* LISTING */
#listing {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 .5em;
display: flex;
padding: 0 0.5em;
flex-wrap: wrap;
justify-content: space-between;
}
#listing.list {
flex-direction: column;
flex-direction: column;
}
#listing .item {
background-color: #fff;
border-radius: .2em;
padding: .5em;
margin: 0 .5em 1em;
border: .2em solid #fff;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.12);
transition: .2s ease all;
flex-grow: 1;
margin: 0 0.5em 1em;
padding: 0.5em;
cursor: pointer;
transition: 0.2s ease all;
border: 0.2em solid #fff;
border-radius: 0.2em;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06), 0 1px 2px rgba(0, 0, 0, .12);
flex-grow: 1;
}
.item:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24) !important;
}
.item.selected {
border-color: #6f6f6f !important;
border-color: #6f6f6f !important;
}
.item div {
display: inline-block;
vertical-align: middle;
display: inline-block;
vertical-align: middle;
}
.item p {
margin: 0;
font-size: .9em;
color: #4e4e4e;
font-size: 0.9em;
margin: 0;
color: #4e4e4e;
}
.item span {
font-weight: bold;
font-weight: bold;
}
.item i {
font-size: 4em;
margin-right: .1em;
font-size: 4em;
margin-right: 0.1em;
}
.item a:hover,
.item a {
color: #6f6f6f;
.item a, .item a:hover {
color: #6f6f6f;
}
/* ANIMATIONS */
i.spin {
animation: 1s spin linear infinite;
animation: 1s spin linear infinite;
}
@keyframes spin {
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}
/* EDITOR */
.editor .frontmatter {
border: 1px solid #ddd;
background: #fff;
}
.editor label {
display: inline-block;
width: 19%;
}
.editor fieldset {
margin: 0;
padding: 0;
border: 0;
background-color: rgba(0, 0, 0, .05);
}
.editor button {
display: none;
}
@keyframes spin { 100% { -webkit-transform: rotate(-360deg); transform:rotate(-360deg); } }

View File

@ -0,0 +1,23 @@
{{ define "content" }}
<div class="editor container {{ .Class }}">
<form method="POST" action="./">
{{ if or (eq .Class "frontmatter-only") (eq .Class "complete") }}
<div class="frontmatter">
{{ template "blocks" .FrontMatter }}
<button class="add">Add field</button>
</div>
{{ end }}
{{ if or (eq .Class "content-only") (eq .Class "complete") }}
<div class="content">
<div id="editor" data-mode="{{ .Mode }}"></div>
<textarea name="content">{{ .Content }}</textarea>
</div>
{{ end }}
<div>
<input type="submit" data-type="{{ .Class }}" value="Save">
</div>
</form>
</div>
{{ end }}

View File

@ -0,0 +1,44 @@
{{ define "blocks" }}
{{ range $key, $value := . }}
{{ if or (eq $value.Type "object") (eq $value.Type "array") }}
<fieldset id="{{ $value.Name }}" data-type="{{ $value.Type }}">
<h3>{{ SplitCapitalize $value.Title }}</h3>
<span class="actions">
<button class="add">&#43;</button>
<button class="delete">&#8722;</button>
</span>
{{ template "blocks" $value.Content }}
</fieldset>
{{ else }}
{{ if not (eq $value.Parent.Type "array") }}
<div class="block" id="block-{{ $value.Name }}" data-content="{{ $value.Name }}">
<label for="{{ $value.Name }}">{{ SplitCapitalize $value.Title }}</label>
<span class="actions">
<button class="delete">&#8722;</button>
</span>
{{ end }}
{{ if eq $value.Parent.Type "array" }}
<div id="{{ $value.Name }}-{{ $key }}" data-type="array-item">
{{ end }}
{{ if eq $value.HTMLType "textarea" }}
<textarea class="scroll" name="{{ $value.Name }}:{{ $value.Type }}" id="{{ $value.Name }}" data-parent-type="{{ $value.Parent.Type }}">{{ $value.Content }}</textarea>
{{ else if eq $value.HTMLType "datetime" }}
<input name="{{ $value.Name }}:{{ $value.Type }}" id="{{ $value.Name }}" value="{{ $value.Content.Format "2006-01-02T15:04" }}" type="datetime-local" data-parent-type="{{ $value.Parent.Type }}"></input>
{{ else }}
<input name="{{ $value.Name }}:{{ $value.Type }}" id="{{ $value.Name }}" value="{{ $value.Content }}" type="{{ $value.HTMLType }}" data-parent-type="{{ $value.Parent.Type }}"></input>
{{ end }}
{{ if not (eq $value.Parent.Type "array") }}</div>{{ end }}
{{ if eq $value.Parent.Type "array" }}
<span class="actions"><button class="delete">&#8722;</button></span></div>
{{ end }}
{{ end }}
{{ end }}
{{ end }}

View File

@ -4,6 +4,8 @@
// assets/public/js/application.js
// assets/templates/actions.tmpl
// assets/templates/base.tmpl
// assets/templates/editor.tmpl
// assets/templates/frontmatter.tmpl
// assets/templates/listing.tmpl
// assets/templates/single.tmpl
// DO NOT EDIT!
@ -104,6 +106,42 @@ func templatesBaseTmpl() (*asset, error) {
return a, err
}
// templatesEditorTmpl reads file data from disk. It returns an error on failure.
func templatesEditorTmpl() (*asset, error) {
path := "D:\\Code\\Go\\src\\github.com\\hacdias\\caddy-filemanager\\assets\\templates\\editor.tmpl"
name := "templates/editor.tmpl"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// templatesFrontmatterTmpl reads file data from disk. It returns an error on failure.
func templatesFrontmatterTmpl() (*asset, error) {
path := "D:\\Code\\Go\\src\\github.com\\hacdias\\caddy-filemanager\\assets\\templates\\frontmatter.tmpl"
name := "templates/frontmatter.tmpl"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// templatesListingTmpl reads file data from disk. It returns an error on failure.
func templatesListingTmpl() (*asset, error) {
path := "D:\\Code\\Go\\src\\github.com\\hacdias\\caddy-filemanager\\assets\\templates\\listing.tmpl"
@ -196,6 +234,8 @@ var _bindata = map[string]func() (*asset, error){
"public/js/application.js": publicJsApplicationJs,
"templates/actions.tmpl": templatesActionsTmpl,
"templates/base.tmpl": templatesBaseTmpl,
"templates/editor.tmpl": templatesEditorTmpl,
"templates/frontmatter.tmpl": templatesFrontmatterTmpl,
"templates/listing.tmpl": templatesListingTmpl,
"templates/single.tmpl": templatesSingleTmpl,
}
@ -251,6 +291,8 @@ var _bintree = &bintree{nil, map[string]*bintree{
"templates": &bintree{nil, map[string]*bintree{
"actions.tmpl": &bintree{templatesActionsTmpl, map[string]*bintree{}},
"base.tmpl": &bintree{templatesBaseTmpl, map[string]*bintree{}},
"editor.tmpl": &bintree{templatesEditorTmpl, map[string]*bintree{}},
"frontmatter.tmpl": &bintree{templatesFrontmatterTmpl, map[string]*bintree{}},
"listing.tmpl": &bintree{templatesListingTmpl, map[string]*bintree{}},
"single.tmpl": &bintree{templatesSingleTmpl, map[string]*bintree{}},
}},

124
editor.go Normal file
View File

@ -0,0 +1,124 @@
package filemanager
import (
"bytes"
"path/filepath"
"strings"
"github.com/spf13/hugo/parser"
)
// Editor contains the information for the editor page
type Editor struct {
Class string
Mode string
Content string
FrontMatter interface{}
}
// GetEditor gets the editor based on a FileInfo struct
func (fi FileInfo) GetEditor() (*Editor, error) {
// Create a new editor variable and set the mode
editor := new(Editor)
editor.Mode = strings.TrimPrefix(filepath.Ext(fi.Name), ".")
switch editor.Mode {
case "md", "markdown", "mdown", "mmark":
editor.Mode = "markdown"
case "asciidoc", "adoc", "ad":
editor.Mode = "asciidoc"
case "rst":
editor.Mode = "rst"
case "html", "htm":
editor.Mode = "html"
case "js":
editor.Mode = "javascript"
}
var page parser.Page
var err error
// Handle the content depending on the file extension
switch editor.Mode {
case "markdown", "asciidoc", "rst":
if editor.hasFrontMatterRune(fi.Raw) {
// Starts a new buffer and parses the file using Hugo's functions
buffer := bytes.NewBuffer(fi.Raw)
page, err = parser.ReadFrom(buffer)
if err != nil {
return editor, err
}
// Parses the page content and the frontmatter
editor.Content = strings.TrimSpace(string(page.Content()))
editor.FrontMatter, _, err = Pretty(page.FrontMatter())
editor.Class = "complete"
} else {
// The editor will handle only content
editor.Class = "content-only"
editor.Content = fi.Content
}
case "json", "toml", "yaml":
// Defines the class and declares an error
editor.Class = "frontmatter-only"
// Checks if the file already has the frontmatter rune and parses it
if editor.hasFrontMatterRune(fi.Raw) {
editor.FrontMatter, _, err = Pretty(fi.Raw)
} else {
editor.FrontMatter, _, err = Pretty(editor.appendFrontMatterRune(fi.Raw, editor.Mode))
}
// Check if there were any errors
if err != nil {
return editor, err
}
default:
// The editor will handle only content
editor.Class = "content-only"
editor.Content = fi.Content
}
return editor, nil
}
func (e Editor) hasFrontMatterRune(file []byte) bool {
return strings.HasPrefix(string(file), "---") ||
strings.HasPrefix(string(file), "+++") ||
strings.HasPrefix(string(file), "{")
}
func (e Editor) appendFrontMatterRune(frontmatter []byte, language string) []byte {
switch language {
case "yaml":
return []byte("---\n" + string(frontmatter) + "\n---")
case "toml":
return []byte("+++\n" + string(frontmatter) + "\n+++")
case "json":
return frontmatter
}
return frontmatter
}
// CanBeEdited checks if the extension of a file is supported by the editor
func CanBeEdited(filename string) bool {
extensions := [...]string{
"md", "markdown", "mdown", "mmark",
"asciidoc", "adoc", "ad",
"rst",
".json", ".toml", ".yaml",
".css", ".sass", ".scss",
".js",
".html",
".txt",
}
for _, extension := range extensions {
if strings.HasSuffix(filename, extension) {
return true
}
}
return false
}

View File

@ -27,6 +27,7 @@ type FileInfo struct {
Mode os.FileMode
Mimetype string
Content string
Raw []byte
Type string
}
@ -85,6 +86,7 @@ func (fi *FileInfo) Read() error {
}
fi.Mimetype = http.DetectContentType(raw)
fi.Content = string(raw)
fi.Raw = raw
return nil
}
@ -164,6 +166,17 @@ func (fi *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *C
},
}
if CanBeEdited(fi.Name) {
editor, err := fi.GetEditor()
if err != nil {
return http.StatusInternalServerError, err
}
page.Info.Data = editor
return page.PrintAsHTML(w, "frontmatter", "editor")
}
return page.PrintAsHTML(w, "single")
}

173
frontmatter.go Normal file
View File

@ -0,0 +1,173 @@
package filemanager
import (
"log"
"reflect"
"sort"
"strings"
"github.com/hacdias/caddy-filemanager/variables"
"github.com/spf13/cast"
"github.com/spf13/hugo/parser"
)
const (
mainName = "#MAIN#"
objectType = "object"
arrayType = "array"
)
var mainTitle = ""
// Pretty creates a new FrontMatter object
func Pretty(content []byte) (interface{}, string, error) {
frontType := parser.DetectFrontMatter(rune(content[0]))
front, err := frontType.Parse(content)
if err != nil {
return []string{}, mainTitle, err
}
object := new(frontmatter)
object.Type = objectType
object.Name = mainName
return rawToPretty(front, object), mainTitle, nil
}
type frontmatter struct {
Name string
Title string
Content interface{}
Type string
HTMLType string
Parent *frontmatter
}
func rawToPretty(config interface{}, parent *frontmatter) interface{} {
objects := []*frontmatter{}
arrays := []*frontmatter{}
fields := []*frontmatter{}
cnf := map[string]interface{}{}
if reflect.TypeOf(config) == reflect.TypeOf(map[interface{}]interface{}{}) {
for key, value := range config.(map[interface{}]interface{}) {
cnf[key.(string)] = value
}
} else if reflect.TypeOf(config) == reflect.TypeOf([]interface{}{}) {
for key, value := range config.([]interface{}) {
cnf[string(key)] = value
}
} else {
cnf = config.(map[string]interface{})
}
for name, element := range cnf {
if variables.IsMap(element) {
objects = append(objects, handleObjects(element, parent, name))
} else if variables.IsSlice(element) {
arrays = append(arrays, handleArrays(element, parent, name))
} else {
if name == "title" && parent.Name == mainName {
mainTitle = element.(string)
}
fields = append(fields, handleFlatValues(element, parent, name))
}
}
sort.Sort(sortByTitle(objects))
sort.Sort(sortByTitle(arrays))
sort.Sort(sortByTitle(fields))
settings := []*frontmatter{}
settings = append(settings, fields...)
settings = append(settings, arrays...)
settings = append(settings, objects...)
return settings
}
type sortByTitle []*frontmatter
func (f sortByTitle) Len() int { return len(f) }
func (f sortByTitle) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f sortByTitle) Less(i, j int) bool {
return strings.ToLower(f[i].Name) < strings.ToLower(f[j].Name)
}
func handleObjects(content interface{}, parent *frontmatter, name string) *frontmatter {
c := new(frontmatter)
c.Parent = parent
c.Type = objectType
c.Title = name
if parent.Name == mainName {
c.Name = c.Title
} else if parent.Type == arrayType {
c.Name = parent.Name + "[]"
} else {
c.Name = parent.Name + "[" + c.Title + "]"
}
c.Content = rawToPretty(content, c)
return c
}
func handleArrays(content interface{}, parent *frontmatter, name string) *frontmatter {
c := new(frontmatter)
c.Parent = parent
c.Type = arrayType
c.Title = name
if parent.Name == mainName {
c.Name = name
} else {
c.Name = parent.Name + "[" + name + "]"
}
c.Content = rawToPretty(content, c)
return c
}
func handleFlatValues(content interface{}, parent *frontmatter, name string) *frontmatter {
c := new(frontmatter)
c.Parent = parent
switch reflect.ValueOf(content).Kind() {
case reflect.Bool:
c.Type = "boolean"
case reflect.Int, reflect.Float32, reflect.Float64:
c.Type = "number"
default:
c.Type = "string"
}
c.Content = content
switch strings.ToLower(name) {
case "description":
c.HTMLType = "textarea"
case "date", "publishdate":
c.HTMLType = "datetime"
c.Content = cast.ToTime(content)
default:
c.HTMLType = "text"
}
if parent.Type == arrayType {
c.Name = parent.Name + "[]"
c.Title = content.(string)
} else if parent.Type == objectType {
c.Title = name
c.Name = parent.Name + "[" + name + "]"
if parent.Name == mainName {
c.Name = name
}
} else {
log.Panic("Parent type not allowed in handleFlatValues.")
}
return c
}

11
page.go
View File

@ -7,6 +7,8 @@ import (
"log"
"net/http"
"strings"
"github.com/hacdias/caddy-filemanager/variables"
)
// Page contains the informations and functions needed to show the page
@ -73,6 +75,13 @@ func (p PageInfo) PreviousLink() string {
// PrintAsHTML formats the page in HTML and executes the template
func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) {
// Create the functions map, then the template, check for erros and
// execute the template if there aren't errors
functions := template.FuncMap{
"SplitCapitalize": variables.SplitCapitalize,
"Defined": variables.Defined,
}
templates = append(templates, "actions", "base")
var tpl *template.Template
@ -90,7 +99,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
// If it's the first iteration, creates a new template and add the
// functions map
if i == 0 {
tpl, err = template.New(t).Parse(string(page))
tpl, err = template.New(t).Funcs(functions).Parse(string(page))
} else {
tpl, err = tpl.Parse(string(page))
}

42
variables/transform.go Normal file
View File

@ -0,0 +1,42 @@
package variables
import (
"strings"
"unicode"
)
var splitCapitalizeExceptions = map[string]string{
"youtube": "YouTube",
"github": "GitHub",
"googleplus": "Google Plus",
"linkedin": "LinkedIn",
}
// SplitCapitalize splits a string by its uppercase letters and capitalize the
// first letter of the string
func SplitCapitalize(name string) string {
if val, ok := splitCapitalizeExceptions[strings.ToLower(name)]; ok {
return val
}
var words []string
l := 0
for s := name; s != ""; s = s[l:] {
l = strings.IndexFunc(s[1:], unicode.IsUpper) + 1
if l <= 0 {
l = len(s)
}
words = append(words, s[:l])
}
name = ""
for _, element := range words {
name += element + " "
}
name = strings.ToLower(name[:len(name)-1])
name = strings.ToUpper(string(name[0])) + name[1:]
return name
}

View File

@ -0,0 +1,31 @@
package variables
import "testing"
type testSplitCapitalize struct {
name string
result string
}
var testSplitCapitalizeCases = []testSplitCapitalize{
{"loremIpsum", "Lorem ipsum"},
{"LoremIpsum", "Lorem ipsum"},
{"loremipsum", "Loremipsum"},
{"YouTube", "YouTube"},
{"GitHub", "GitHub"},
{"GooglePlus", "Google Plus"},
{"Facebook", "Facebook"},
}
func TestSplitCapitalize(t *testing.T) {
for _, pair := range testSplitCapitalizeCases {
v := SplitCapitalize(pair.name)
if v != pair.result {
t.Error(
"For", pair.name,
"expected", pair.result,
"got", v,
)
}
}
}

13
variables/types.go Normal file
View File

@ -0,0 +1,13 @@
package variables
import "reflect"
// IsMap checks if some variable is a map
func IsMap(sth interface{}) bool {
return reflect.ValueOf(sth).Kind() == reflect.Map
}
// IsSlice checks if some variable is a slice
func IsSlice(sth interface{}) bool {
return reflect.ValueOf(sth).Kind() == reflect.Slice
}

37
variables/variables.go Normal file
View File

@ -0,0 +1,37 @@
package variables
import (
"errors"
"log"
"reflect"
)
// Defined checks if variable is defined in a struct
func Defined(data interface{}, field string) bool {
t := reflect.Indirect(reflect.ValueOf(data)).Type()
if t.Kind() != reflect.Struct {
log.Print("Non-struct type not allowed.")
return false
}
_, b := t.FieldByName(field)
return b
}
// Dict allows to send more than one variable into a template
func Dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}

View File

@ -0,0 +1,41 @@
package variables
import "testing"
type testDefinedData struct {
f1 string
f2 bool
f3 int
f4 func()
}
type testDefined struct {
data interface{}
field string
result bool
}
var testDefinedCases = []testDefined{
{testDefinedData{}, "f1", true},
{testDefinedData{}, "f2", true},
{testDefinedData{}, "f3", true},
{testDefinedData{}, "f4", true},
{testDefinedData{}, "f5", false},
{[]string{}, "", false},
{map[string]int{"oi": 4}, "", false},
{"asa", "", false},
{"int", "", false},
}
func TestDefined(t *testing.T) {
for _, pair := range testDefinedCases {
v := Defined(pair.data, pair.field)
if v != pair.result {
t.Error(
"For", pair.data,
"expected", pair.result,
"got", v,
)
}
}
}