|
- package caddyhugo
-
- import (
- "encoding/base64"
- "fmt"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "strings"
-
- "git.stephensearles.com/stephen/acedoc"
-
- "github.com/caddyserver/caddy/caddyhttp/httpserver"
- )
-
- func (t *tmplData) Content() ([]Content, error) {
- return GetContent(t.Site.Root, t.HugoSites)
- }
-
- 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) {
- 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
- }
-
- type tmplData struct {
- Site *httpserver.SiteConfig
- R *http.Request
- *CaddyHugo
- Doc *acedoc.Document
- docref *editSession
- }
-
- func (t *tmplData) LoadContent() (string, error) {
- return t.Doc.Contents(), nil
- }
-
- 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))
- }
-
- var EditPage = `<html>
- <head>
- <script src="/hugo/simplemde.js"></script>
- <style type="text/css" media="screen">
- #editor-wrapper {
- position: absolute;
- top: 50px;
- right: 0;
- bottom: 150px;
- left: 40%;
- }
- #draft {
- position: absolute;
- top: 50px;
- right: 60%;
- bottom: 0;
- left: 0;
-
- }
- #draft > iframe {
- height: 100%;
- width: 100%;
- border: none;
- }
- a {
- text-decoration: underline;
- text-decoration-style: dotted;
- cursor: pointer;
- }
- </style>
- <link rel="stylesheet" href="/hugo/simplemde.css" />
- <script src="/hugo/vue.js"></script>
- <script src="/hugo/moment.js"></script>
-
-
- <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>
- <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;
- }
-
-
- // Create WebSocket connection.
- var socket = connect();
-
-
- 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;
- }
-
- 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);
- })
-
- 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;
- }
- </script>
- </body>
- </html>`
-
- var AdminPage = `<html><body>not implemented</body></html>`
-
- var AuthorPage = `<html>
- <head>
- </head>
- <body>
- {{ $timeFormat := "Jan _2 15:04:05" }}
- <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>
- </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)
- }
|