You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
caddy-hugo2/templates.go

529 lines
13 KiB

8 years ago
package caddyhugo
import (
"encoding/base64"
8 years ago
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"strings"
8 years ago
"git.stephensearles.com/stephen/acedoc"
8 years ago
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func (t *tmplData) Content() ([]Content, error) {
return GetContent(t.Site.Root, t.HugoSites)
8 years ago
}
func (t *tmplData) ContentTypes() ([]string, error) {
nameMap := map[string]struct{}{"default": struct{}{}}
names, err := t.contentTypes(path.Join(t.Site.Root, "archetypes"))
if err != nil {
return nil, err
}
for _, name := range names {
nameMap[name] = struct{}{}
}
names, err = t.contentTypes(path.Join(t.Site.Root, "themes", "hugo-theme-minos", "archetypes"))
if err != nil {
return nil, err
}
for _, name := range names {
nameMap[name] = struct{}{}
}
var out []string
for name := range nameMap {
out = append(out, name[:len(name)-len(filepath.Ext(name))])
}
return out, nil
}
func (t *tmplData) contentTypes(dir string) ([]string, error) {
8 years ago
layoutDir, err := os.Open(path.Join(t.Site.Root, "archetypes"))
if err != nil {
fmt.Println("opening layout dir", err)
return nil, err
}
defer layoutDir.Close()
names, err := layoutDir.Readdirnames(0)
if err != nil {
fmt.Println("reading dir", err)
return nil, err
}
return names, nil
8 years ago
}
type tmplData struct {
Site *httpserver.SiteConfig
R *http.Request
*CaddyHugo
Doc *acedoc.Document
docref *editSession
8 years ago
}
func (t *tmplData) LoadContent() (string, error) {
return t.Doc.Contents(), nil
8 years ago
}
func baseNoExt(name string) string {
base := path.Base(name)
return base[:len(base)-len(path.Ext(base))]
}
func (t *tmplData) IframeSource() string {
name := baseNoExt(t.docref.docname)
ctype := baseNoExt(path.Dir(t.docref.docname))
if ctype == "content" {
return fmt.Sprintf("/hugo/draft/%s/%s/", base64.RawURLEncoding.EncodeToString([]byte(t.docref.docname)), strings.Replace(name, " ", "-", -1))
}
return fmt.Sprintf("/hugo/draft/%s/%s/%s/", base64.RawURLEncoding.EncodeToString([]byte(t.docref.docname)), ctype, strings.Replace(name, " ", "-", -1))
}
8 years ago
var EditPage = `<html>
<head>
<script src="/hugo/simplemde.js"></script>
8 years ago
<style type="text/css" media="screen">
#editor-wrapper {
8 years ago
position: absolute;
top: 50px;
right: 0;
bottom: 150px;
left: 40%;
}
#draft {
position: absolute;
top: 50px;
right: 60%;
bottom: 0;
8 years ago
left: 0;
8 years ago
}
#draft > iframe {
height: 100%;
width: 100%;
border: none;
}
a {
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
}
8 years ago
</style>
<link rel="stylesheet" href="/hugo/simplemde.css" />
<script src="/hugo/vue.js"></script>
<script src="/hugo/moment.js"></script>
8 years ago
<body>
<div id="container" >
<div id="header">
<div id="lastSaved">
<span v-if="sendQueue.length > 0 || Object.keys(needConfirmation).length > 0">last saved ${ lastSaved.from(now) }, saving</span>
<span v-else>saved</span>
<span v-if="connectionError">, ${connectionError}</span>
</div>
<div>
<a id="sideview-toggle-media">media</a>
<a id="sideview-toggle-draft">draft</a>
</div>
</div>
<div id="editor-wrapper">
<textarea id="editor">{{ .LoadContent }}</textarea>
</div>
<div id="draft"><iframe src="{{ .IframeSource }}">Loading draft...</iframe></div>
</div>
8 years ago
<script>
var iframe = document.querySelector("#draft > iframe");
document.onclick = function (event) {
var iframe = document.querySelector("#draft > iframe");
switch (event.target.id) {
case "sideview-toggle-media":
iframe.src = "/hugo/media";
break;
case "sideview-toggle-draft":
iframe.src = "{{ .IframeSource }}";
break;
}
}
var uiBindings = {
ltime: {{ .LTime }},
serverLtime: 0,
lastSaved: moment(),
now: moment(),
connectionError: null,
sendQueue: [],
sentRecently: [],
needConfirmation: {},
};
var app = new Vue({
el: "#container",
data: uiBindings,
delimiters: ["${", "}"],
});
function getLtime() {
uiBindings.ltime++
return uiBindings.ltime
}
function observeServer(l, confirmed) {
uiBindings.serverLtime = l;
if (confirmed && confirmed.length > 0) {
confirmed.forEach(function (e) {
delete uiBindings.needConfirmation[e];
})
}
observe(l);
}
function observe(l) {
if (l > uiBindings.ltime) {
uiBindings.now = moment();
uiBindings.lastSaved = moment();
uiBindings.ltime = l;
}
}
var selectedImage;
var editorElem = document.getElementById("editor");
var editor = new SimpleMDE({
element: editorElem,
forceSync: true,
insertTexts: {
image: ["{\{% thumb filename=\"", "#url#\" width=\"200\" %}}"]
},
imageURLFn: function () {
return selectedImage;
}
});
window.onmessage = function (evt) {
selectedImage = evt.data;
}
8 years ago
8 years ago
// Create WebSocket connection.
var socket = connect();
8 years ago
const sawChangesBumpsTo = 10;
var sentinelSrc = 'about:blank';
var oldSrc = '';
var sawChanges = -1;
window.setInterval(function () {
if (sawChanges >= 0) {
sawChanges--;
if (sawChanges == 0) {
if (iframe.contentWindow) {
iframe.contentWindow.location.reload();
}
}
}
uiBindings.now = moment();
if (uiBindings.connectionError) {
socket = connect();
} else if (uiBindings.sendQueue.length > 0) {
var ltime = getLtime();
// record lowest pending
// ltime at the time this message
// was serialized
var lowestPending = ltime;
for (c in uiBindings.needConfirmation) {
c = parseInt(c, 10);
if (lowestPending === 0 || c < lowestPending) {
lowestPending = c;
}
}
var msg = JSON.stringify({
"deltas": uiBindings.sendQueue,
"ltime": ltime,
"lowestPending": lowestPending,
});
uiBindings.sendQueue = [];
uiBindings.needConfirmation[ltime] = msg;
}
for (ltime in uiBindings.needConfirmation) {
var msg = uiBindings.needConfirmation[ltime];
socket.send(msg);
}
}, 500);
function connect() {
const socket = new WebSocket((location.protocol == "https:" ? 'wss://' : 'ws://') + location.host + location.pathname);
// Listen for messages
socket.addEventListener('message', function (event) {
var message = JSON.parse(event.data);
observeServer(message.ltime, message.confirmed);
var deltas = [];
deltas.push.apply(deltas, message.deltas);
deltas.forEach(function(aceDelta) {
var cmDelta = aceDeltaToCM(aceDelta)
var content = ""
var to = {
line: aceDelta.start.row,
ch: aceDelta.start.column,
}
if (aceDelta.action == "insert") {
content = aceDelta.lines.join("\n");
to = null;
}
editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
sawChanges = sawChangesBumpsTo;
})
});
socket.addEventListener('open', function () {
uiBindings.connectionError = null;
});
socket.addEventListener('close', function () {
if (!uiBindings.connectionError) {
getLtime();
uiBindings.connectionError = "server connection closed, reconnecting...";
}
});
socket.addEventListener('error', function (err) {
if (!uiBindings.connectionError) {
uiBindings.connectionError = err;
getLtime();
}
console.log(err);
});
return socket;
}
8 years ago
editor.codemirror.on("change", function (cm, cmDelta) {
if (cmDelta.origin == "dontreflect") {
return;
}
var aceDelta = cmDeltaToAce(cmDelta);
console.log(cmDelta, "=>", aceDelta)
sawChanges = sawChangesBumpsTo;
uiBindings.sendQueue.push.apply(uiBindings.sendQueue, aceDelta);
8 years ago
})
function cmDeltaToAce(cmDelta) {
var isRemove = (cmDelta.removed.length > 0 && cmDelta.removed[0].length > 0) || cmDelta.removed.length > 1;
var lines = isRemove ? cmDelta.removed : cmDelta.text;
var aceDelta = {
action: isRemove ? "remove" : "insert",
lines: lines,
start: {
row: cmDelta.from.line,
column: cmDelta.from.ch,
},
end: {
row: cmDelta.from.line + (isRemove ? lines.length - 1 : lines.length - 1 ),
column: lines[lines.length-1].length,
}
};
if (aceDelta.start.row == aceDelta.end.row) {
aceDelta.end.column += cmDelta.from.ch;
}
if (false && isRemove && aceDelta.start.row == aceDelta.end.row) {
var origStart = aceDelta.start;
aceDelta.start = aceDelta.end;
aceDelta.end = origStart;
aceDelta.start.column += cmDelta.from.ch;
}
if (isRemove && ((cmDelta.text.length > 0 && cmDelta.text[0].length > 0) || cmDelta.text.length > 1)) {
cmDelta.removed = [""];
var ret = [aceDelta];
ret.push.apply(ret, cmDeltaToAce(cmDelta));
return ret;
}
return [aceDelta];
}
function aceDeltaToCM(aceDelta) {
var cmDelta = {
text: [],
removed: [],
from: {
line: aceDelta.start.row,
ch: aceDelta.start.column,
},
to: {
// cm deltas are weird. to refers to the selection end, which
// with a simple blinking cursor with no selection, is always
// the same as from
line: aceDelta.start.row,
ch: aceDelta.start.column,
},
}
if (aceDelta.action == "remove") {
var origStart = aceDelta.start;
aceDelta.start = aceDelta.end;
aceDelta.end = origStart;
cmDelta.removed = aceDelta.lines
cmDelta.text = [""]
} else {
cmDelta.text = aceDelta.lines
cmDelta.removed = [""]
}
return cmDelta;
}
8 years ago
</script>
</body>
</html>`
var AdminPage = `<html><body>not implemented</body></html>`
var AuthorPage = `<html>
<head>
</head>
<body>
{{ $timeFormat := "Jan _2 15:04:05" }}
8 years ago
<p>Create content:</p>
<form action="/hugo/edit/new" method="POST">
<label>Name: <input type="text" name="name" /></label>
<select name="type">
{{- range .ContentTypes }}
<option value="{{ . }}">{{ . }}</option>
{{- end }}
</select>
<input type="submit" />
</form>
<p>Edit content:</p>
<table>{{ range .Content }}
<tr>
{{ if .Metadata }}
<td>
<a href="/hugo/edit/{{ .Filename }}">
{{ .Metadata.Title }}
</a>
</td>
<td>
{{ .Metadata.Date.Format $timeFormat }}
{{ if not (.Metadata.Lastmod.Equal .Metadata.Date) }}
(last modified {{.Metadata.Lastmod.Format $timeFormat }})
{{end}}
</td>
{{ else }}
<td>
<a href="/hugo/edit/{{ .Filename }}">
{{ .Filename }}
</a>
</td>
<td>(unable to load metadata)</td>
{{ end }}
</tr>
{{- end }}
</table>
8 years ago
</body>
</html>`
func UploadPage(elemName string) string {
return fmt.Sprintf(`
<input type="file" style="display: hidden;" id="%s" />
<div id="%s_dropzone" ondrop="dropHandler(event);" ondragover="draghandler(event);" ondragenter="draghandler(event);" ondragleave="draghandler(event);" style="background-color: rgba(0,0,0,0.5); visibility: hidden; opacity:0; position: fixed; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; ; transition: visibility 175ms, opacity 175ms; z-index: 9999999;"></div>
<script>
document.addEventListener("DOMContentLoaded", function () {
var fileInput = document.getElementById('%s');
var dropzone = document.getElementById('%s_dropzone');
fileInput.onchange = function () {
var formData = new FormData();
fileInput.files.forEach(function (file) {
formData.append(file.name, file);
});
upload(formData);
}
var lastTarget = null;
window.addEventListener("dragenter", function(e)
{
lastTarget = e.target; // cache the last target here
// unhide our dropzone overlay
dropzone.style.visibility = "";
dropzone.style.opacity = 1;
});
window.addEventListener("dragleave", function(e)
{
// this is the magic part. when leaving the window,
// e.target happens to be exactly what we want: what we cached
// at the start, the dropzone we dragged into.
// so..if dragleave target matches our cache, we hide the dropzone.
if(e.target === lastTarget)
{
dropzone.style.visibility = "hidden";
dropzone.style.opacity = 0;
}
});
});
function draghandler(evt) {
evt.preventDefault();
}
function dropHandler(evt) {
evt.preventDefault();
var files = evt.dataTransfer.files;
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
formData.append(files[i].name, files[i]);
}
upload(formData);
return false;
}
function upload(formData) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if ( 4 == this.readyState ) {
window.location.reload(true);
}
}
xhr.open('POST', '/hugo/upload');
xhr.send(formData);
}
</script>
`, elemName, elemName, elemName, elemName, elemName, elemName)
}