diff --git a/assets/public/css/styles.css b/assets/public/css/styles.css index 64f12246..ea5bdb4a 100644 --- a/assets/public/css/styles.css +++ b/assets/public/css/styles.css @@ -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); } } diff --git a/assets/templates/editor.tmpl b/assets/templates/editor.tmpl new file mode 100644 index 00000000..53d324a1 --- /dev/null +++ b/assets/templates/editor.tmpl @@ -0,0 +1,23 @@ +{{ define "content" }} +
+
+ {{ if or (eq .Class "frontmatter-only") (eq .Class "complete") }} +
+ {{ template "blocks" .FrontMatter }} + +
+ {{ end }} + + {{ if or (eq .Class "content-only") (eq .Class "complete") }} +
+
+ +
+ {{ end }} + +
+ +
+
+
+{{ end }} diff --git a/assets/templates/frontmatter.tmpl b/assets/templates/frontmatter.tmpl new file mode 100644 index 00000000..81eaaab3 --- /dev/null +++ b/assets/templates/frontmatter.tmpl @@ -0,0 +1,44 @@ +{{ define "blocks" }} +{{ range $key, $value := . }} + + {{ if or (eq $value.Type "object") (eq $value.Type "array") }} +
+

{{ SplitCapitalize $value.Title }}

+ + + + + {{ template "blocks" $value.Content }} +
+ {{ else }} + + {{ if not (eq $value.Parent.Type "array") }} +
+ + + + + + {{ end }} + + {{ if eq $value.Parent.Type "array" }} +
+ {{ end }} + + {{ if eq $value.HTMLType "textarea" }} + + {{ else if eq $value.HTMLType "datetime" }} + + {{ else }} + + {{ end }} + + {{ if not (eq $value.Parent.Type "array") }}
{{ end }} + + {{ if eq $value.Parent.Type "array" }} +
+ {{ end }} + +{{ end }} +{{ end }} +{{ end }} diff --git a/binary.go b/binary.go index 437018ce..ec585562 100644 --- a/binary.go +++ b/binary.go @@ -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{}}, }}, diff --git a/editor.go b/editor.go new file mode 100644 index 00000000..b2fef581 --- /dev/null +++ b/editor.go @@ -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 +} diff --git a/fileinfo.go b/fileinfo.go index 4d78c947..728ad60b 100644 --- a/fileinfo.go +++ b/fileinfo.go @@ -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") } diff --git a/frontmatter.go b/frontmatter.go new file mode 100644 index 00000000..25aae8be --- /dev/null +++ b/frontmatter.go @@ -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 +} diff --git a/page.go b/page.go index dcf0ebbe..ad931adf 100644 --- a/page.go +++ b/page.go @@ -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)) } diff --git a/variables/transform.go b/variables/transform.go new file mode 100644 index 00000000..3307b7a3 --- /dev/null +++ b/variables/transform.go @@ -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 +} diff --git a/variables/transform_test.go b/variables/transform_test.go new file mode 100644 index 00000000..1a0c2fbf --- /dev/null +++ b/variables/transform_test.go @@ -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, + ) + } + } +} diff --git a/variables/types.go b/variables/types.go new file mode 100644 index 00000000..ee43dad3 --- /dev/null +++ b/variables/types.go @@ -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 +} diff --git a/variables/variables.go b/variables/variables.go new file mode 100644 index 00000000..9d60dabf --- /dev/null +++ b/variables/variables.go @@ -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 +} diff --git a/variables/variables_test.go b/variables/variables_test.go new file mode 100644 index 00000000..ec76d459 --- /dev/null +++ b/variables/variables_test.go @@ -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, + ) + } + } +}