This commit is contained in:
Henrique Dias
2016-06-21 15:28:15 +01:00
parent 8879f22932
commit b43d8e2465
67 changed files with 977 additions and 3381 deletions

201
_stuff/LICENSE.md Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

37
_stuff/README.md Normal file
View File

@@ -0,0 +1,37 @@
# hugo - a caddy plugin
[![Build](https://img.shields.io/travis/hacdias/caddy-hugo.svg?style=flat-square)](https://travis-ci.org/hacdias/caddy-hugo)
[![community](https://img.shields.io/badge/community-forum-ff69b4.svg?style=flat-square)](https://forum.caddyserver.com)
[![Documentation](https://img.shields.io/badge/caddy-doc-F06292.svg?style=flat-square)](https://caddyserver.com/docs/hugo)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/hacdias/caddy-hugo)
[Hugo](http://gohugo.io/) is an easy to use and fast command line static website generator, while [Caddy](http://caddyserver.com) is a lightweight, fast, general-purpose, cross-platform HTTP/2 web server with automatic HTTPS. This extension is able to bring a web interface to Caddy to manage Hugo generated websites. This plugin provides you an web interface to manage your websites made with Hugo.
**If you're not developer go to the [documentation](https://caddyserver.com/docs/hugo)**.
## Build from source
Requirements:
+ [Go 1.6 or higher][1]
+ [caddydev][2]
+ [go-bindata][3]
+ [Node.js w/ npm][4] (optional)
Instructions:
1. ```go get github.com/hacdias/caddy-hugo```
2. ```cd $GOPATH/github.com/hacdias/caddy-hugo```
1. If you want to modify the CSS/JS:
2. Change the third comment to ```//go:generate go-bindata -debug -pkg assets -o assets/assets.go templates/ assets/css/ assets/js/ assets/fonts/```
3. ```npm install```
4. ```grunt watch```
3. ```go generate```
4. ```cd $YOUR_WEBSITE_PATH```
5. ```caddydev --source $GOPATH/github.com/hacdias/caddy-hugo hugo```
6. Go to ```http://domain:port```
[1]: https://golang.org/dl/
[2]: https://github.com/caddyserver/caddydev
[3]: https://github.com/jteeuwen/go-bindata
[4]: https://nodejs.org

161
_stuff/editor.go Normal file
View File

@@ -0,0 +1,161 @@
package hugo
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/hacdias/caddy-hugo/tools/frontmatter"
"github.com/hacdias/caddy-hugo/tools/templates"
"github.com/hacdias/caddy-hugo/tools/variables"
"github.com/spf13/hugo/parser"
)
type editor struct {
Name string
Class string
IsPost bool
Mode string
Content string
FrontMatter interface{}
Config *Config
}
// GET handles the GET method on editor page
func GET(w http.ResponseWriter, r *http.Request, c *Config, filename string) (int, error) {
// Check if the file format is supported. If not, send a "Not Acceptable"
// header and an error
if !templates.CanBeEdited(filename) {
return http.StatusNotAcceptable, errors.New("File format not supported.")
}
// Check if the file exists.
if _, err := os.Stat(filename); os.IsNotExist(err) {
return http.StatusNotFound, err
} else if os.IsPermission(err) {
return http.StatusForbidden, err
} else if err != nil {
return http.StatusInternalServerError, err
}
// Open the file and check if there was some error while opening
file, err := ioutil.ReadFile(filename)
if os.IsPermission(err) {
return http.StatusForbidden, err
} else if err != nil {
return http.StatusInternalServerError, err
}
// Create a new editor variable and set the extension
page := new(editor)
page.Mode = strings.TrimPrefix(filepath.Ext(filename), ".")
page.Name = strings.Replace(filename, c.Path, "", 1)
page.Config = c
page.IsPost = false
// Sanitize the extension
page.Mode = sanitizeMode(page.Mode)
var ppage parser.Page
// Handle the content depending on the file extension
switch page.Mode {
case "markdown", "asciidoc", "rst":
if hasFrontMatterRune(file) {
// Starts a new buffer and parses the file using Hugo's functions
buffer := bytes.NewBuffer(file)
ppage, err = parser.ReadFrom(buffer)
if err != nil {
return http.StatusInternalServerError, err
}
if strings.Contains(string(ppage.FrontMatter()), "date") {
page.IsPost = true
}
// Parses the page content and the frontmatter
page.Content = strings.TrimSpace(string(ppage.Content()))
page.FrontMatter, page.Name, err = frontmatter.Pretty(ppage.FrontMatter())
page.Class = "complete"
} else {
// The editor will handle only content
page.Class = "content-only"
page.Content = string(file)
}
case "json", "toml", "yaml":
// Defines the class and declares an error
page.Class = "frontmatter-only"
// Checks if the file already has the frontmatter rune and parses it
if hasFrontMatterRune(file) {
page.FrontMatter, _, err = frontmatter.Pretty(file)
} else {
page.FrontMatter, _, err = frontmatter.Pretty(appendFrontMatterRune(file, page.Mode))
}
// Check if there were any errors
if err != nil {
return http.StatusInternalServerError, err
}
default:
// The editor will handle only content
page.Class = "content-only"
page.Content = string(file)
}
// 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,
}
tpl, err := templates.Get(r, functions, "editor", "frontmatter")
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, tpl.Execute(w, page)
}
func hasFrontMatterRune(file []byte) bool {
return strings.HasPrefix(string(file), "---") ||
strings.HasPrefix(string(file), "+++") ||
strings.HasPrefix(string(file), "{")
}
func 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
}
func sanitizeMode(extension string) string {
switch extension {
case "md", "markdown", "mdown", "mmark":
return "markdown"
case "asciidoc", "adoc", "ad":
return "asciidoc"
case "rst":
return "rst"
case "html", "htm":
return "html"
case "js":
return "javascript"
default:
return extension
}
}

66
_stuff/git.go Normal file
View File

@@ -0,0 +1,66 @@
package hugo
import (
"bytes"
"encoding/json"
"net/http"
"os/exec"
"strings"
)
// HandleGit handles the POST method on GIT page which is only an API.
func HandleGit(w http.ResponseWriter, r *http.Request, c *Config) (int, error) {
response := &Response{
Code: http.StatusOK,
Err: nil,
Content: "OK",
}
// Check if git is installed on the computer
if _, err := exec.LookPath("git"); err != nil {
response.Code = http.StatusNotImplemented
response.Content = "Git is not installed on your computer."
return response.Send(w)
}
// Get the JSON information sent using a buffer
buff := new(bytes.Buffer)
buff.ReadFrom(r.Body)
// Creates the raw file "map" using the JSON
var info map[string]interface{}
json.Unmarshal(buff.Bytes(), &info)
// Check if command was sent
if _, ok := info["command"]; !ok {
response.Code = http.StatusBadRequest
response.Content = "Command not specified."
return response.Send(w)
}
command := info["command"].(string)
args := strings.Split(command, " ")
if len(args) > 0 && args[0] == "git" {
args = append(args[:0], args[1:]...)
}
if len(args) == 0 {
response.Code = http.StatusBadRequest
response.Content = "Command not specified."
return response.Send(w)
}
cmd := exec.Command("git", args...)
cmd.Dir = c.Path
output, err := cmd.CombinedOutput()
if err != nil {
response.Code = http.StatusInternalServerError
response.Content = err.Error()
return response.Send(w)
}
response.Content = string(output)
return response.Send(w)
}

98
_stuff/hugo.go Normal file
View File

@@ -0,0 +1,98 @@
// Package hugo makes the bridge between the static website generator Hugo
// and the webserver Caddy, also providing an administrative user interface.
package hugo
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/hacdias/caddy-filemanager"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// Hugo contais the next middleware to be run and the configuration
// of the current one.
type Hugo struct {
FileManager *filemanager.FileManager
Next httpserver.Handler
Config *Config
}
// ServeHTTP is the main function of the whole plugin that routes every single
// request to its function.
func (h Hugo) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Check if the current request if for this plugin
if httpserver.Path(r.URL.Path).Matches(h.Config.Admin) {
// If this request requires a raw file or a download, return the FileManager
query := r.URL.Query()
if val, ok := query["raw"]; ok && val[0] == "true" {
return h.FileManager.ServeHTTP(w, r)
}
if val, ok := query["download"]; ok && val[0] == "true" {
return h.FileManager.ServeHTTP(w, r)
}
// If the url matches exactly with /{admin}/settings/, redirect
// to the page of the configuration file
if r.URL.Path == h.Config.Admin+"/settings/" {
var frontmatter string
if _, err := os.Stat(h.Config.Path + "config.yaml"); err == nil {
frontmatter = "yaml"
}
if _, err := os.Stat(h.Config.Path + "config.json"); err == nil {
frontmatter = "json"
}
if _, err := os.Stat(h.Config.Path + "config.toml"); err == nil {
frontmatter = "toml"
}
http.Redirect(w, r, h.Config.Admin+"/config."+frontmatter, http.StatusTemporaryRedirect)
return 0, nil
}
filename := strings.Replace(r.URL.Path, c.Admin+"/edit/", "", 1)
filename = c.Path + filename
if strings.HasPrefix(r.URL.Path, h.Config.Admin+"/api/git/") && r.Method == http.MethodPost {
return HandleGit(w, r, h.Config)
}
if h.ShouldHandle(r.URL) {
// return editor
return 0, nil
}
return h.FileManager.ServeHTTP(w, r)
}
return h.Next.ServeHTTP(w, r)
}
var extensions = []string{
"md", "markdown", "mdown", "mmark",
"asciidoc", "adoc", "ad",
"rst",
"html", "htm",
"js",
"toml", "yaml", "json",
}
// ShouldHandle checks if this extension should be handled by this plugin
func (h Hugo) ShouldHandle(url *url.URL) bool {
extension := strings.TrimPrefix(filepath.Ext(url.Path), ".")
for _, ext := range extensions {
if ext == extension {
return true
}
}
return false
}

119
_stuff/page.go Normal file
View File

@@ -0,0 +1,119 @@
package hugo
import (
"encoding/json"
"html/template"
"log"
"net/http"
"strings"
)
// Page contains the informations and functions needed to show the page
type Page struct {
Info *PageInfo
}
// PageInfo contains the information of a page
type PageInfo struct {
Name string
Path string
IsDir bool
Config *Config
Data interface{}
}
// BreadcrumbMap returns p.Path where every element is a map
// of URLs and path segment names.
func (p PageInfo) BreadcrumbMap() map[string]string {
result := map[string]string{}
if len(p.Path) == 0 {
return result
}
// skip trailing slash
lpath := p.Path
if lpath[len(lpath)-1] == '/' {
lpath = lpath[:len(lpath)-1]
}
parts := strings.Split(lpath, "/")
for i, part := range parts {
if i == 0 && part == "" {
// Leading slash (root)
result["/"] = "/"
continue
}
result[strings.Join(parts[:i+1], "/")] = part
}
return result
}
// PreviousLink returns the path of the previous folder
func (p PageInfo) PreviousLink() string {
parts := strings.Split(strings.TrimSuffix(p.Path, "/"), "/")
if len(parts) <= 1 {
return ""
}
if parts[len(parts)-2] == "" {
if p.Config.BaseURL == "" {
return "/"
}
return p.Config.BaseURL
}
return parts[len(parts)-2]
}
// PrintAsHTML formats the page in HTML and executes the template
func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) {
templates = append(templates, "actions", "base")
var tpl *template.Template
// For each template, add it to the the tpl variable
for i, t := range templates {
// Get the template from the assets
page, err := Asset("templates/" + t + ".tmpl")
// Check if there is some error. If so, the template doesn't exist
if err != nil {
log.Print(err)
return http.StatusInternalServerError, err
}
// 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))
} else {
tpl, err = tpl.Parse(string(page))
}
if err != nil {
log.Print(err)
return http.StatusInternalServerError, err
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, p.Info)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// PrintAsJSON prints the current page infromation in JSON
func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
marsh, err := json.Marshal(p.Info.Data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
return w.Write(marsh)
}

28
_stuff/response.go Normal file
View File

@@ -0,0 +1,28 @@
package hugo
import (
"encoding/json"
"net/http"
)
// Response conta
type Response struct {
Code int
Err error
Content string
}
// Send used to send JSON responses to the web server
func (r *Response) Send(w http.ResponseWriter) (int, error) {
content := map[string]string{"message": r.Content}
msg, msgErr := json.Marshal(content)
if msgErr != nil {
return 500, msgErr
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(r.Code)
w.Write(msg)
return 0, r.Err
}

29
_stuff/run.go Normal file
View File

@@ -0,0 +1,29 @@
package hugo
import (
"log"
"os"
"github.com/hacdias/caddy-hugo/tools/commands"
"github.com/hacdias/caddy-hugo/tools/variables"
)
// Run is used to run the static website generator
func Run(c *Config, force bool) {
os.RemoveAll(c.Path + "public")
// Prevent running if watching is enabled
if b, pos := variables.StringInSlice("--watch", c.Args); b && !force {
if len(c.Args) > pos && c.Args[pos+1] != "false" {
return
}
if len(c.Args) == pos+1 {
return
}
}
if err := commands.Run(c.Hugo, c.Args, c.Path); err != nil {
log.Panic(err)
}
}

145
_stuff/setup.go Normal file
View File

@@ -0,0 +1,145 @@
package hugo
import (
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/hacdias/caddy-filemanager"
"github.com/hacdias/caddy-hugo/tools/commands"
"github.com/hacdias/caddy-hugo/tools/installer"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// Setup is the init function of Caddy plugins and it configures the whole
// middleware thing.
func setup(c *caddy.Controller) error {
cnf := httpserver.GetConfig(c.Key)
conf, _ := ParseHugo(c, cnf.Root)
// Checks if there is an Hugo website in the path that is provided.
// If not, a new website will be created.
create := true
if _, err := os.Stat(conf.Path + "config.yaml"); err == nil {
create = false
}
if _, err := os.Stat(conf.Path + "config.json"); err == nil {
create = false
}
if _, err := os.Stat(conf.Path + "config.toml"); err == nil {
create = false
}
if create {
err := commands.Run(conf.Hugo, []string{"new", "site", conf.Path, "--force"}, ".")
if err != nil {
log.Panic(err)
}
}
// Generates the Hugo website for the first time the plugin is activated.
go Run(conf, true)
mid := func(next httpserver.Handler) httpserver.Handler {
return &Hugo{
Next: next,
FileManager: &filemanager.FileManager{
Next: next,
Configs: []filemanager.Config{
filemanager.Config{
PathScope: conf.Path,
Root: http.Dir(conf.Path),
BaseURL: conf.Admin,
},
},
},
Config: conf,
}
}
cnf.AddMiddleware(mid)
return nil
}
// Config is the add-on configuration set on Caddyfile
type Config struct {
Public string // Public content path
Path string // Hugo files path
Styles string // Admin styles path
Args []string // Hugo arguments
Hugo string // Hugo executable path
Admin string // Hugo admin URL
Git bool // Is this site a git repository
}
// ParseHugo parses the configuration file
func ParseHugo(c *caddy.Controller, root string) (*Config, error) {
conf := &Config{
Public: strings.Replace(root, "./", "", -1),
Admin: "/admin",
Path: "./",
Git: false,
}
conf.Hugo = installer.GetPath()
for c.Next() {
args := c.RemainingArgs()
switch len(args) {
case 1:
conf.Path = args[0]
conf.Path = strings.TrimSuffix(conf.Path, "/")
conf.Path += "/"
}
for c.NextBlock() {
switch c.Val() {
case "styles":
if !c.NextArg() {
return nil, c.ArgErr()
}
conf.Styles = c.Val()
// Remove the beginning slash if it exists or not
conf.Styles = strings.TrimPrefix(conf.Styles, "/")
// Add a beginning slash to make a
conf.Styles = "/" + conf.Styles
case "admin":
if !c.NextArg() {
return nil, c.ArgErr()
}
conf.Admin = c.Val()
conf.Admin = strings.TrimPrefix(conf.Admin, "/")
conf.Admin = "/" + conf.Admin
default:
key := "--" + c.Val()
value := "true"
if c.NextArg() {
value = c.Val()
}
conf.Args = append(conf.Args, key+"="+value)
}
}
}
if _, err := os.Stat(filepath.Join(conf.Path, ".git")); err == nil {
conf.Git = true
}
return conf, nil
}

0
_stuff/templates.go Normal file
View File

View File

@@ -0,0 +1,15 @@
package commands
import (
"os"
"os/exec"
)
// Run executes an external command
func Run(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@@ -0,0 +1,173 @@
package frontmatter
import (
"log"
"reflect"
"sort"
"strings"
"github.com/hacdias/caddy-hugo/tools/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
}

View File

@@ -0,0 +1,25 @@
package installer
var (
sha256Hash = map[string]string{
"hugo_0.16_darwin-arm32.tgz": "683d5d4b4e0ac03a183ca5eb9019981ba696569445c7d6d1efc7e6706bd273a5",
"hugo_0.16_dragonfly-64bit.tgz": "63a3ee9a36d4d2166c77b96bb8bf39b2239affe118e44a83b3d0a44374a8921d",
"hugo_0.16_freebsd-32bit.tgz": "ea3f84900feeeb9d89573dea49a4349753116e70de561eeec4858f7ffc74f8f9",
"hugo_0.16_freebsd-64bit.tgz": "8d9320bb660090a77a4f922ca30b1582593bc6d87c3fd8bd6f5ecbe49cf1d2f2",
"hugo_0.16_freebsd-arm32.tgz": "b4c21296e01ea68709ac50d7eb1d314b738f1c8408ff2be223d06ae76604dbea",
"hugo_0.16_linux-32bit.tgz": "aed82d156f01a4562c39bd1af41aa81699009140da965e0369c370ba874725c9",
"hugo_0.16_linux-64bit.tgz": "13e299dc45bea4fad5bdf8c2640305a5926e2acd02c3aa03b7864403e513920e",
"hugo_0.16_linux-arm32.tgz": "bc836def127d93e2457da9994f9c09b0100523e46d61074cd724ef092b11714f",
"hugo_0.16_linux-arm64.tgz": "d04486918f43f89f1e0359eebedd8a05d96f7ca40f93e7fd8d7c3f2dac115a8d",
"hugo_0.16_netbsd-32bit.tgz": "cb578eebec5b6364b0afd5bb208d94317acab0a3e033b81f04b1511af0669b63",
"hugo_0.16_netbsd-64bit.tgz": "d3c766d9800d7fdd268ffd2f28b7af451f13a4de63901bfdae2ee5c96528b8cc",
"hugo_0.16_netbsd-arm32.tgz": "51162b2637e71b786582af715a44b778f62bdc62a9a354ccc4a7c8384afe194c",
"hugo_0.16_openbsd-32bit.tgz": "2d1e112a7346850897ea77da868c0d987ef90efb7f49c917659437a5a67f89f8",
"hugo_0.16_openbsd-64bit.tgz": "7b33ff2565df5a6253c3e4308813d947e34af04c633fb4e01cac83751066e16e",
"hugo_0.16_osx-32bit.tgz": "6155dda548bbd1e26c26a4a00472e4c0e55fad9fcd46991ce90987385bd5fd0a",
"hugo_0.16_osx-64bit.tgz": "b0cba8f6996946ef34a664184d6461567d79fc2a3e793145d34379902eda0ad9",
"hugo_0.16_solaris-64bit.tgz": "af9557403af5e16eb7faf965c04540417a70699efbbbc4e0a7ae4c4703ad1ae8",
"hugo_0.16_windows-32bit.zip": "1c72d06843fe02cb62348660d87a523c885ed684a683271fc8762e7234c4210b",
"hugo_0.16_windows-64bit.zip": "a3fda0bd30592e4eb3bdde85c8a8ce23a7433073536466d16fd0e97bf7794067",
}
)

View File

@@ -0,0 +1,216 @@
package installer
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"github.com/hacdias/caddy-hugo/tools/files"
"github.com/mitchellh/go-homedir"
"github.com/pivotal-golang/archiver/extractor"
)
const (
version = "0.16"
baseurl = "https://github.com/spf13/hugo/releases/download/v" + version + "/"
)
var caddy, bin, temp, hugo, tempfile, zipname, exename string
// GetPath retrives the Hugo path for the user or install it if it's not found
func GetPath() string {
initializeVariables()
var err error
// Check if Hugo is already on $PATH
if hugo, err = exec.LookPath("hugo"); err == nil {
if checkVersion() {
return hugo
}
}
// Check if Hugo is on $HOME/.caddy/bin
if _, err = os.Stat(hugo); err == nil {
if checkVersion() {
return hugo
}
}
fmt.Println("Unable to find Hugo on your computer.")
// Create the neccessary folders
os.MkdirAll(caddy, 0774)
os.Mkdir(bin, 0774)
if temp, err = ioutil.TempDir("", "caddy-hugo"); err != nil {
fmt.Println(err)
os.Exit(-1)
}
downloadHugo()
checkSHA256()
fmt.Print("Unzipping... ")
// Unzip or Ungzip the file
switch runtime.GOOS {
case "darwin", "windows":
zp := extractor.NewZip()
err = zp.Extract(tempfile, temp)
default:
gz := extractor.NewTgz()
err = gz.Extract(tempfile, temp)
}
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
fmt.Println("done.")
var exetorename string
err = filepath.Walk(temp, func(path string, f os.FileInfo, err error) error {
if f.Name() == exename {
exetorename = path
}
return nil
})
// Copy the file
fmt.Print("Moving Hugo executable... ")
err = files.CopyFile(exetorename, hugo)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
err = os.Chmod(hugo, 0755)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
fmt.Println("done.")
fmt.Println("Hugo installed at " + hugo)
defer os.RemoveAll(temp)
return hugo
}
func initializeVariables() {
var arch string
switch runtime.GOARCH {
case "amd64":
arch = "64bit"
case "386":
arch = "32bit"
case "arm":
arch = "arm32"
default:
arch = runtime.GOARCH
}
var ops = runtime.GOOS
if runtime.GOOS == "darwin" && runtime.GOARCH != "arm" {
ops = "osx"
}
exename = "hugo"
zipname = "hugo_" + version + "_" + ops + "-" + arch
homedir, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
caddy = filepath.Join(homedir, ".caddy")
bin = filepath.Join(caddy, "bin")
hugo = filepath.Join(bin, "hugo")
switch runtime.GOOS {
case "windows":
zipname += ".zip"
exename += ".exe"
hugo += ".exe"
default:
zipname += ".tgz"
}
}
func checkVersion() bool {
out, _ := exec.Command("hugo", "version").Output()
r := regexp.MustCompile(`v\d\.\d{2}`)
v := r.FindStringSubmatch(string(out))[0]
v = v[1:len(v)]
return (v == version)
}
func downloadHugo() {
tempfile = filepath.Join(temp, zipname)
fmt.Print("Downloading Hugo from GitHub releases... ")
// Create the file
out, err := os.Create(tempfile)
out.Chmod(0774)
if err != nil {
defer os.RemoveAll(temp)
fmt.Println(err)
os.Exit(-1)
}
defer out.Close()
// Get the data
resp, err := http.Get(baseurl + zipname)
if err != nil {
fmt.Println("An error ocurred while downloading. If this error persists, try downloading Hugo from \"https://github.com/spf13/hugo/releases/\" and put the executable in " + bin + " and rename it to 'hugo' or 'hugo.exe' if you're on Windows.")
fmt.Println(err)
os.Exit(-1)
}
defer resp.Body.Close()
// Writer the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
fmt.Println("downloaded.")
}
func checkSHA256() {
fmt.Print("Checking SHA256...")
hasher := sha256.New()
f, err := os.Open(tempfile)
if err != nil {
log.Fatal(err)
}
defer f.Close()
if _, err := io.Copy(hasher, f); err != nil {
log.Fatal(err)
}
if hex.EncodeToString(hasher.Sum(nil)) != sha256Hash[zipname] {
fmt.Println("can't verify SHA256.")
os.Exit(-1)
}
fmt.Println("checked!")
}

View File

@@ -0,0 +1,30 @@
package server
import (
"encoding/json"
"net/http"
)
type Response struct {
Code int
Err error
Content interface{}
}
// RespondJSON used to send JSON responses to the web server
func RespondJSON(w http.ResponseWriter, r *Response) (int, error) {
if r.Content == nil {
r.Content = map[string]string{}
}
msg, msgErr := json.Marshal(r.Content)
if msgErr != nil {
return 500, msgErr
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(r.Code)
w.Write(msg)
return 0, r.Err
}

View File

@@ -0,0 +1,26 @@
package server
import (
"net/http"
"strings"
)
// ParseURLComponents parses the components of an URL creating an array
func ParseURLComponents(r *http.Request) []string {
//The URL that the user queried.
path := r.URL.Path
path = strings.TrimSpace(path)
//Cut off the leading and trailing forward slashes, if they exist.
//This cuts off the leading forward slash.
if strings.HasPrefix(path, "/") {
path = path[1:]
}
//This cuts off the trailing forward slash.
if strings.HasSuffix(path, "/") {
cutOffLastCharLen := len(path) - 1
path = path[:cutOffLastCharLen]
}
//We need to isolate the individual components of the path.
components := strings.Split(path, "/")
return components
}

View File

@@ -0,0 +1,73 @@
package templates
import (
"log"
"net/http"
"strings"
"text/template"
"github.com/hacdias/caddy-hugo/routes/assets"
)
// 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
}
// Get is used to get a ready to use template based on the url and on
// other sent templates
func Get(r *http.Request, functions template.FuncMap, templates ...string) (*template.Template, error) {
// If this is a pjax request, use the minimal template to send only
// the main content
if r.Header.Get("X-PJAX") == "true" {
templates = append(templates, "base_minimal")
} else {
templates = append(templates, "base_full")
}
var tpl *template.Template
// For each template, add it to the the tpl variable
for i, t := range templates {
// Get the template from the assets
page, err := assets.Asset("templates/" + t + ".tmpl")
// Check if there is some error. If so, the template doesn't exist
if err != nil {
log.Print(err)
return new(template.Template), err
}
// If it's the first iteration, creates a new template and add the
// functions map
if i == 0 {
tpl, err = template.New(t).Funcs(functions).Parse(string(page))
} else {
tpl, err = tpl.Parse(string(page))
}
if err != nil {
log.Print(err)
return new(template.Template), err
}
}
return tpl, nil
}

View File

@@ -0,0 +1,39 @@
package templates
import "testing"
type canBeEdited struct {
file string
result bool
}
var canBeEditedPairs = []canBeEdited{
{"file.markdown", true},
{"file.md", true},
{"file.json", true},
{"file.toml", true},
{"file.yaml", true},
{"file.css", true},
{"file.sass", true},
{"file.scss", true},
{"file.js", true},
{"file.html", true},
{"file.git", false},
{"file.log", false},
{"file.sh", false},
{"file.png", false},
{"file.jpg", false},
}
func TestCanBeEdited(t *testing.T) {
for _, pair := range canBeEditedPairs {
v := CanBeEdited(pair.file)
if v != pair.result {
t.Error(
"For", pair.file,
"expected", pair.result,
"got", v,
)
}
}
}

View File

@@ -0,0 +1,10 @@
package variables
func StringInSlice(a string, list []string) (bool, int) {
for i, b := range list {
if b == a {
return true, i
}
}
return false, 0
}

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,
)
}
}
}

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
}

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,
)
}
}
}

24
_stuff/utilities.go Normal file
View File

@@ -0,0 +1,24 @@
package hugo
import (
"encoding/json"
"net/http"
)
// RespondJSON used to send JSON responses to the web server
func RespondJSON(w http.ResponseWriter, message interface{}, code int, err error) (int, error) {
if message == nil {
message = map[string]string{}
}
msg, msgErr := json.Marshal(message)
if msgErr != nil {
return 500, msgErr
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(msg)
return 0, err
}