No Description
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.

templates.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. package caddyhugo
  2. import (
  3. "encoding/base64"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "path"
  8. "path/filepath"
  9. "strings"
  10. "git.stephensearles.com/stephen/acedoc"
  11. "github.com/mholt/caddy/caddyhttp/httpserver"
  12. )
  13. func (t *tmplData) Content() ([]Content, error) {
  14. return GetContent(t.Site.Root, t.HugoSites)
  15. }
  16. func (t *tmplData) ContentTypes() ([]string, error) {
  17. nameMap := map[string]struct{}{"default": struct{}{}}
  18. names, err := t.contentTypes(path.Join(t.Site.Root, "archetypes"))
  19. if err != nil {
  20. return nil, err
  21. }
  22. for _, name := range names {
  23. nameMap[name] = struct{}{}
  24. }
  25. names, err = t.contentTypes(path.Join(t.Site.Root, "themes", "hugo-theme-minos", "archetypes"))
  26. if err != nil {
  27. return nil, err
  28. }
  29. for _, name := range names {
  30. nameMap[name] = struct{}{}
  31. }
  32. var out []string
  33. for name := range nameMap {
  34. out = append(out, name[:len(name)-len(filepath.Ext(name))])
  35. }
  36. return out, nil
  37. }
  38. func (t *tmplData) contentTypes(dir string) ([]string, error) {
  39. layoutDir, err := os.Open(path.Join(t.Site.Root, "archetypes"))
  40. if err != nil {
  41. fmt.Println("opening layout dir", err)
  42. return nil, err
  43. }
  44. defer layoutDir.Close()
  45. names, err := layoutDir.Readdirnames(0)
  46. if err != nil {
  47. fmt.Println("reading dir", err)
  48. return nil, err
  49. }
  50. return names, nil
  51. }
  52. type tmplData struct {
  53. Site *httpserver.SiteConfig
  54. R *http.Request
  55. *CaddyHugo
  56. Doc *acedoc.Document
  57. docref *editSession
  58. }
  59. func (t *tmplData) LoadContent() (string, error) {
  60. return t.Doc.Contents(), nil
  61. }
  62. func baseNoExt(name string) string {
  63. base := path.Base(name)
  64. return base[:len(base)-len(path.Ext(base))]
  65. }
  66. func (t *tmplData) IframeSource() string {
  67. name := baseNoExt(t.docref.docname)
  68. ctype := baseNoExt(path.Dir(t.docref.docname))
  69. if ctype == "content" {
  70. return fmt.Sprintf("/hugo/draft/%s/%s/", base64.RawURLEncoding.EncodeToString([]byte(t.docref.docname)), strings.Replace(name, " ", "-", -1))
  71. }
  72. return fmt.Sprintf("/hugo/draft/%s/%s/%s/", base64.RawURLEncoding.EncodeToString([]byte(t.docref.docname)), ctype, strings.Replace(name, " ", "-", -1))
  73. }
  74. var EditPage = `<html>
  75. <head>
  76. <script src="/hugo/simplemde.js"></script>
  77. <style type="text/css" media="screen">
  78. #editor-wrapper {
  79. position: absolute;
  80. top: 50px;
  81. right: 0;
  82. bottom: 150px;
  83. left: 40%;
  84. }
  85. #draft {
  86. position: absolute;
  87. top: 50px;
  88. right: 60%;
  89. bottom: 0;
  90. left: 0;
  91. }
  92. #draft > iframe {
  93. height: 100%;
  94. width: 100%;
  95. border: none;
  96. }
  97. a {
  98. text-decoration: underline;
  99. text-decoration-style: dotted;
  100. cursor: pointer;
  101. }
  102. </style>
  103. <link rel="stylesheet" href="/hugo/simplemde.css" />
  104. <script src="/hugo/vue.js"></script>
  105. <script src="/hugo/moment.js"></script>
  106. <body>
  107. <div id="container" >
  108. <div id="header">
  109. <div id="lastSaved">
  110. <span v-if="sendQueue.length > 0 || Object.keys(needConfirmation).length > 0">last saved ${ lastSaved.from(now) }, saving</span>
  111. <span v-else>saved</span>
  112. <span v-if="connectionError">, ${connectionError}</span>
  113. </div>
  114. <div>
  115. <a id="sideview-toggle-media">media</a>
  116. <a id="sideview-toggle-draft">draft</a>
  117. </div>
  118. </div>
  119. <div id="editor-wrapper">
  120. <textarea id="editor">{{ .LoadContent }}</textarea>
  121. </div>
  122. <div id="draft"><iframe src="{{ .IframeSource }}">Loading draft...</iframe></div>
  123. </div>
  124. <script>
  125. var iframe = document.querySelector("#draft > iframe");
  126. document.onclick = function (event) {
  127. var iframe = document.querySelector("#draft > iframe");
  128. switch (event.target.id) {
  129. case "sideview-toggle-media":
  130. iframe.src = "/hugo/media";
  131. break;
  132. case "sideview-toggle-draft":
  133. iframe.src = "{{ .IframeSource }}";
  134. break;
  135. }
  136. }
  137. var uiBindings = {
  138. ltime: {{ .LTime }},
  139. serverLtime: 0,
  140. lastSaved: moment(),
  141. now: moment(),
  142. connectionError: null,
  143. sendQueue: [],
  144. sentRecently: [],
  145. needConfirmation: {},
  146. };
  147. var app = new Vue({
  148. el: "#container",
  149. data: uiBindings,
  150. delimiters: ["${", "}"],
  151. });
  152. function getLtime() {
  153. uiBindings.ltime++
  154. return uiBindings.ltime
  155. }
  156. function observeServer(l, confirmed) {
  157. uiBindings.serverLtime = l;
  158. if (confirmed && confirmed.length > 0) {
  159. confirmed.forEach(function (e) {
  160. delete uiBindings.needConfirmation[e];
  161. })
  162. }
  163. observe(l);
  164. }
  165. function observe(l) {
  166. if (l > uiBindings.ltime) {
  167. uiBindings.now = moment();
  168. uiBindings.lastSaved = moment();
  169. uiBindings.ltime = l;
  170. }
  171. }
  172. var selectedImage;
  173. var editorElem = document.getElementById("editor");
  174. var editor = new SimpleMDE({
  175. element: editorElem,
  176. forceSync: true,
  177. insertTexts: {
  178. image: ["{\{% thumb filename=\"", "#url#\" width=\"200\" %}}"]
  179. },
  180. imageURLFn: function () {
  181. return selectedImage;
  182. }
  183. });
  184. window.onmessage = function (evt) {
  185. selectedImage = evt.data;
  186. }
  187. // Create WebSocket connection.
  188. var socket = connect();
  189. const sawChangesBumpsTo = 10;
  190. var sentinelSrc = 'about:blank';
  191. var oldSrc = '';
  192. var sawChanges = -1;
  193. window.setInterval(function () {
  194. if (sawChanges >= 0) {
  195. sawChanges--;
  196. if (sawChanges == 0) {
  197. if (iframe.contentWindow) {
  198. iframe.contentWindow.location.reload();
  199. }
  200. }
  201. }
  202. uiBindings.now = moment();
  203. if (uiBindings.connectionError) {
  204. socket = connect();
  205. } else if (uiBindings.sendQueue.length > 0) {
  206. var ltime = getLtime();
  207. // record lowest pending
  208. // ltime at the time this message
  209. // was serialized
  210. var lowestPending = ltime;
  211. for (c in uiBindings.needConfirmation) {
  212. c = parseInt(c, 10);
  213. if (lowestPending === 0 || c < lowestPending) {
  214. lowestPending = c;
  215. }
  216. }
  217. var msg = JSON.stringify({
  218. "deltas": uiBindings.sendQueue,
  219. "ltime": ltime,
  220. "lowestPending": lowestPending,
  221. });
  222. uiBindings.sendQueue = [];
  223. uiBindings.needConfirmation[ltime] = msg;
  224. }
  225. for (ltime in uiBindings.needConfirmation) {
  226. var msg = uiBindings.needConfirmation[ltime];
  227. socket.send(msg);
  228. }
  229. }, 500);
  230. function connect() {
  231. const socket = new WebSocket((location.protocol == "https:" ? 'wss://' : 'ws://') + location.host + location.pathname);
  232. // Listen for messages
  233. socket.addEventListener('message', function (event) {
  234. var message = JSON.parse(event.data);
  235. observeServer(message.ltime, message.confirmed);
  236. var deltas = [];
  237. deltas.push.apply(deltas, message.deltas);
  238. deltas.forEach(function(aceDelta) {
  239. var cmDelta = aceDeltaToCM(aceDelta)
  240. var content = ""
  241. var to = {
  242. line: aceDelta.start.row,
  243. ch: aceDelta.start.column,
  244. }
  245. if (aceDelta.action == "insert") {
  246. content = aceDelta.lines.join("\n");
  247. to = null;
  248. }
  249. editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
  250. sawChanges = sawChangesBumpsTo;
  251. })
  252. });
  253. socket.addEventListener('open', function () {
  254. uiBindings.connectionError = null;
  255. });
  256. socket.addEventListener('close', function () {
  257. if (!uiBindings.connectionError) {
  258. getLtime();
  259. uiBindings.connectionError = "server connection closed, reconnecting...";
  260. }
  261. });
  262. socket.addEventListener('error', function (err) {
  263. if (!uiBindings.connectionError) {
  264. uiBindings.connectionError = err;
  265. getLtime();
  266. }
  267. console.log(err);
  268. });
  269. return socket;
  270. }
  271. editor.codemirror.on("change", function (cm, cmDelta) {
  272. if (cmDelta.origin == "dontreflect") {
  273. return;
  274. }
  275. var aceDelta = cmDeltaToAce(cmDelta);
  276. console.log(cmDelta, "=>", aceDelta)
  277. sawChanges = sawChangesBumpsTo;
  278. uiBindings.sendQueue.push.apply(uiBindings.sendQueue, aceDelta);
  279. })
  280. function cmDeltaToAce(cmDelta) {
  281. var isRemove = (cmDelta.removed.length > 0 && cmDelta.removed[0].length > 0) || cmDelta.removed.length > 1;
  282. var lines = isRemove ? cmDelta.removed : cmDelta.text;
  283. var aceDelta = {
  284. action: isRemove ? "remove" : "insert",
  285. lines: lines,
  286. start: {
  287. row: cmDelta.from.line,
  288. column: cmDelta.from.ch,
  289. },
  290. end: {
  291. row: cmDelta.from.line + (isRemove ? lines.length - 1 : lines.length - 1 ),
  292. column: lines[lines.length-1].length,
  293. }
  294. };
  295. if (aceDelta.start.row == aceDelta.end.row) {
  296. aceDelta.end.column += cmDelta.from.ch;
  297. }
  298. if (false && isRemove && aceDelta.start.row == aceDelta.end.row) {
  299. var origStart = aceDelta.start;
  300. aceDelta.start = aceDelta.end;
  301. aceDelta.end = origStart;
  302. aceDelta.start.column += cmDelta.from.ch;
  303. }
  304. if (isRemove && ((cmDelta.text.length > 0 && cmDelta.text[0].length > 0) || cmDelta.text.length > 1)) {
  305. cmDelta.removed = [""];
  306. var ret = [aceDelta];
  307. ret.push.apply(ret, cmDeltaToAce(cmDelta));
  308. return ret;
  309. }
  310. return [aceDelta];
  311. }
  312. function aceDeltaToCM(aceDelta) {
  313. var cmDelta = {
  314. text: [],
  315. removed: [],
  316. from: {
  317. line: aceDelta.start.row,
  318. ch: aceDelta.start.column,
  319. },
  320. to: {
  321. // cm deltas are weird. to refers to the selection end, which
  322. // with a simple blinking cursor with no selection, is always
  323. // the same as from
  324. line: aceDelta.start.row,
  325. ch: aceDelta.start.column,
  326. },
  327. }
  328. if (aceDelta.action == "remove") {
  329. var origStart = aceDelta.start;
  330. aceDelta.start = aceDelta.end;
  331. aceDelta.end = origStart;
  332. cmDelta.removed = aceDelta.lines
  333. cmDelta.text = [""]
  334. } else {
  335. cmDelta.text = aceDelta.lines
  336. cmDelta.removed = [""]
  337. }
  338. return cmDelta;
  339. }
  340. </script>
  341. </body>
  342. </html>`
  343. var AdminPage = `<html><body>not implemented</body></html>`
  344. var AuthorPage = `<html>
  345. <head>
  346. </head>
  347. <body>
  348. {{ $timeFormat := "Jan _2 15:04:05" }}
  349. <p>Create content:</p>
  350. <form action="/hugo/edit/new" method="POST">
  351. <label>Name: <input type="text" name="name" /></label>
  352. <select name="type">
  353. {{- range .ContentTypes }}
  354. <option value="{{ . }}">{{ . }}</option>
  355. {{- end }}
  356. </select>
  357. <input type="submit" />
  358. </form>
  359. <p>Edit content:</p>
  360. <table>{{ range .Content }}
  361. <tr>
  362. {{ if .Metadata }}
  363. <td>
  364. <a href="/hugo/edit/{{ .Filename }}">
  365. {{ .Metadata.Title }}
  366. </a>
  367. </td>
  368. <td>
  369. {{ .Metadata.Date.Format $timeFormat }}
  370. {{ if not (.Metadata.Lastmod.Equal .Metadata.Date) }}
  371. (last modified {{.Metadata.Lastmod.Format $timeFormat }})
  372. {{end}}
  373. </td>
  374. {{ else }}
  375. <td>
  376. <a href="/hugo/edit/{{ .Filename }}">
  377. {{ .Filename }}
  378. </a>
  379. </td>
  380. <td>(unable to load metadata)</td>
  381. {{ end }}
  382. </tr>
  383. {{- end }}
  384. </table>
  385. </body>
  386. </html>`
  387. func UploadPage(elemName string) string {
  388. return fmt.Sprintf(`
  389. <input type="file" style="display: hidden;" id="%s" />
  390. <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>
  391. <script>
  392. document.addEventListener("DOMContentLoaded", function () {
  393. var fileInput = document.getElementById('%s');
  394. var dropzone = document.getElementById('%s_dropzone');
  395. fileInput.onchange = function () {
  396. var formData = new FormData();
  397. fileInput.files.forEach(function (file) {
  398. formData.append(file.name, file);
  399. });
  400. upload(formData);
  401. }
  402. var lastTarget = null;
  403. window.addEventListener("dragenter", function(e)
  404. {
  405. lastTarget = e.target; // cache the last target here
  406. // unhide our dropzone overlay
  407. dropzone.style.visibility = "";
  408. dropzone.style.opacity = 1;
  409. });
  410. window.addEventListener("dragleave", function(e)
  411. {
  412. // this is the magic part. when leaving the window,
  413. // e.target happens to be exactly what we want: what we cached
  414. // at the start, the dropzone we dragged into.
  415. // so..if dragleave target matches our cache, we hide the dropzone.
  416. if(e.target === lastTarget)
  417. {
  418. dropzone.style.visibility = "hidden";
  419. dropzone.style.opacity = 0;
  420. }
  421. });
  422. });
  423. function draghandler(evt) {
  424. evt.preventDefault();
  425. }
  426. function dropHandler(evt) {
  427. evt.preventDefault();
  428. var files = evt.dataTransfer.files;
  429. var formData = new FormData();
  430. for (var i = 0; i < files.length; i++) {
  431. formData.append(files[i].name, files[i]);
  432. }
  433. upload(formData);
  434. return false;
  435. }
  436. function upload(formData) {
  437. var xhr = new XMLHttpRequest();
  438. xhr.onreadystatechange = function(e) {
  439. if ( 4 == this.readyState ) {
  440. window.location.reload(true);
  441. }
  442. }
  443. xhr.open('POST', '/hugo/upload');
  444. xhr.send(formData);
  445. }
  446. </script>
  447. `, elemName, elemName, elemName, elemName, elemName, elemName)
  448. }