From 86e7ba257984c8a491dc0f22615d56d02afcd4ce Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 8 Nov 2018 00:11:42 -0500 Subject: [PATCH] Add editor This includes the '/' route handler --- app.go | 21 ++ pad.go | 144 +++++++++++ routes.go | 7 + static/img/ic_blogs@2x.png | Bin 0 -> 199 bytes static/img/ic_blogs_dark@2x.png | Bin 0 -> 195 bytes static/img/ic_brightness@2x.png | Bin 0 -> 485 bytes static/img/ic_brightness_dark@2x.png | Bin 0 -> 439 bytes static/img/ic_down_arrow@2x.png | Bin 0 -> 168 bytes static/img/ic_down_arrow_dark@2x.png | Bin 0 -> 160 bytes static/img/ic_edit@2x.png | Bin 0 -> 239 bytes static/img/ic_edit_dark@2x.png | Bin 0 -> 222 bytes static/img/ic_font@2x.png | Bin 0 -> 105 bytes static/img/ic_font_dark@2x.png | Bin 0 -> 110 bytes static/img/ic_info@2x.png | Bin 0 -> 655 bytes static/img/ic_info_dark@2x.png | Bin 0 -> 640 bytes static/img/ic_list@2x.png | Bin 0 -> 94 bytes static/img/ic_list_dark@2x.png | Bin 0 -> 106 bytes static/img/ic_send@2x.png | Bin 0 -> 344 bytes static/img/ic_send_dark@2x.png | Bin 0 -> 432 bytes static/js/h.js | 257 +++++++++++++++++++ templates/pad.tmpl | 358 +++++++++++++++++++++++++++ 21 files changed, 787 insertions(+) create mode 100644 pad.go create mode 100644 static/img/ic_blogs@2x.png create mode 100644 static/img/ic_blogs_dark@2x.png create mode 100644 static/img/ic_brightness@2x.png create mode 100644 static/img/ic_brightness_dark@2x.png create mode 100644 static/img/ic_down_arrow@2x.png create mode 100644 static/img/ic_down_arrow_dark@2x.png create mode 100644 static/img/ic_edit@2x.png create mode 100644 static/img/ic_edit_dark@2x.png create mode 100644 static/img/ic_font@2x.png create mode 100644 static/img/ic_font_dark@2x.png create mode 100644 static/img/ic_info@2x.png create mode 100644 static/img/ic_info_dark@2x.png create mode 100644 static/img/ic_list@2x.png create mode 100644 static/img/ic_list_dark@2x.png create mode 100644 static/img/ic_send@2x.png create mode 100644 static/img/ic_send_dark@2x.png create mode 100644 static/js/h.js create mode 100644 templates/pad.tmpl diff --git a/app.go b/app.go index 9e50d97..349ec1d 100644 --- a/app.go +++ b/app.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "regexp" "syscall" "github.com/gorilla/mux" @@ -37,6 +38,25 @@ type app struct { sessionStore *sessions.CookieStore } +// handleViewHome shows page at root path. Will be the Pad if logged in and the +// catch-all landing page otherwise. +func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { + if app.cfg.App.SingleUser { + // Render blog index + return handleViewCollection(app, w, r) + } + + // Multi-user instance + u := getUserSession(app, r) + if u != nil { + // User is logged in, so show the Pad + return handleViewPad(app, w, r) + } + + // Show landing page + return renderPage(w, "landing.tmpl", pageForReq(app, r)) +} + func pageForReq(app *app, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, @@ -67,6 +87,7 @@ func pageForReq(app *app, r *http.Request) page.StaticPage { } var shttp = http.NewServeMux() +var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") func Serve() { debugPtr := flag.Bool("debug", false, "Enables debug logging.") diff --git a/pad.go b/pad.go new file mode 100644 index 0000000..6a04030 --- /dev/null +++ b/pad.go @@ -0,0 +1,144 @@ +package writefreely + +import ( + "github.com/gorilla/mux" + "github.com/writeas/impart" + "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/page" + "net/http" + "strings" +) + +func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + action := vars["action"] + slug := vars["slug"] + collAlias := vars["collection"] + appData := &struct { + page.StaticPage + Post *RawPost + User *User + Blogs *[]Collection + + Editing bool // True if we're modifying an existing post + EditCollection *Collection // Collection of the post we're editing, if any + }{ + StaticPage: pageForReq(app, r), + Post: &RawPost{Font: "norm"}, + User: getUserSession(app, r), + } + var err error + if appData.User != nil { + appData.Blogs, err = app.db.GetPublishableCollections(appData.User) + if err != nil { + log.Error("Unable to get user's blogs for Pad: %v", err) + } + } + + padTmpl := "pad" + + if action == "" && slug == "" { + // Not editing any post; simply render the Pad + if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + + return nil + } + + // Retrieve post information for editing + appData.Editing = true + // Make sure this isn't cached, so user doesn't accidentally lose data + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT") + if slug != "" { + appData.Post = getRawCollectionPost(app, slug, collAlias) + if appData.Post.OwnerID != appData.User.ID { + // TODO: add ErrForbiddenEditPost message to flashes + return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/edit")]} + } + appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) + if err != nil { + return err + } + } else { + // Editing a floating article + appData.Post = getRawPost(app, action) + appData.Post.Id = action + } + + if appData.Post.Gone { + return ErrPostUnpublished + } else if appData.Post.Found && appData.Post.Content != "" { + // Got the post + } else if appData.Post.Found { + return ErrPostFetchError + } else { + return ErrPostNotFound + } + + if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + return nil +} + +func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + action := vars["action"] + slug := vars["slug"] + collAlias := vars["collection"] + appData := &struct { + page.StaticPage + Post *RawPost + User *User + EditCollection *Collection // Collection of the post we're editing, if any + Flashes []string + NeedsToken bool + }{ + StaticPage: pageForReq(app, r), + Post: &RawPost{Font: "norm"}, + User: getUserSession(app, r), + } + var err error + + if action == "" && slug == "" { + return ErrPostNotFound + } + + // Make sure this isn't cached, so user doesn't accidentally lose data + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "Thu, 28 Jul 1989 12:00:00 GMT") + if slug != "" { + appData.Post = getRawCollectionPost(app, slug, collAlias) + if appData.Post.OwnerID != appData.User.ID { + // TODO: add ErrForbiddenEditPost message to flashes + return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/meta")]} + } + appData.EditCollection, err = app.db.GetCollectionForPad(collAlias) + if err != nil { + return err + } + } else { + // Editing a floating article + appData.Post = getRawPost(app, action) + appData.Post.Id = action + } + appData.NeedsToken = appData.User == nil || appData.User.ID != appData.Post.OwnerID + + if appData.Post.Gone { + return ErrPostUnpublished + } else if appData.Post.Found && appData.Post.Content != "" { + // Got the post + } else if appData.Post.Found { + return ErrPostFetchError + } else { + return ErrPostNotFound + } + appData.Flashes, _ = getSessionFlashes(app, w, r, nil) + + if err = templates["edit-meta"].ExecuteTemplate(w, "edit-meta", appData); err != nil { + log.Error("Unable to execute template: %v", err) + } + return nil +} diff --git a/routes.go b/routes.go index ff84ca2..92462b6 100644 --- a/routes.go +++ b/routes.go @@ -45,6 +45,12 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST") posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST") + if cfg.App.SingleUser { + write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + } else { + write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") + } + // All the existing stuff write.HandleFunc("/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc("/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") @@ -54,4 +60,5 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto // Posts write.HandleFunc("/{post}", handler.Web(handleViewPost, UserLevelOptional)) } + write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) } diff --git a/static/img/ic_blogs@2x.png b/static/img/ic_blogs@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ecd31fc26376ff61942edc83e3812ec72e1691 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DK2I0NkP61P*L=B}9Ryq++Vy^U z``0o;ApM)Z_Vk5DnktjNtY)s^j-IjNoy^HY2Q%jOs+?n8KkZG;Ex)XUgPB(kGzMR8 zxV^WbB0rw(Tphc?rv|21KCkUH59Q7kP61P*A8+WRuEvh@N?FK z`^SGIx(dC&?fov@oqGI01HCI>&%9kxzct-ZVAph<3jxK?!f(KI?x|tQr zbQ8?(vp#yq&zMwtpyBwH{|64-Q7%4kpx*!D1u=a-6V4l)3VQqXb69qCGGA{#eRCrt ui$@3Jyor0)ZO>V-U^!FGJJ0F8pV@v#ay@-=p)VciDh5wiKbLh*2~7Z!aZS?z literal 0 HcmV?d00001 diff --git a/static/img/ic_brightness@2x.png b/static/img/ic_brightness@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..93528181e4bf68a4c16f8ce1828e2aafdf4fc62e GIT binary patch literal 485 zcmV004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00CV|L_t(o!|m8jZo)7S1z-zMS+rcB;uKB@9H1&UKotk7 ziiCQKR^nFu+dI9%>w*-;&N%oPrcx#FjtuXM5;KltU~~ujgA%1CFhh-+Be?i)0N{x9 z_^OE~iJW+fNQ<8b@dVNRYSbAEA%0I(LiG_~gi}8kKp(imE$;A$H!a||a)1@yH30*I zYrNBhodWyc!1^ z0<*;U7;qm{9SAl)224A34Y7YxsPlVGGsZ%tgZ(3bBQh=#N65gk|0Xkp3~Y`~({w_L zA9~HA6(6>W6hZ>lvG{JYt1^HIzFNoa`P=nQcRhI4TyQ4A*`E~{VvSFYMSqMbme_a` z^YUc(uK;=LDmM@p{%iBMP!f?8pS}q!0wOV<*~S)dd_1=iw+%tWv#$~?92d{O46$51 b576TWj0M8(6>r*O00000NkvXXu0mjfP}jM- literal 0 HcmV?d00001 diff --git a/static/img/ic_brightness_dark@2x.png b/static/img/ic_brightness_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..abef5bd8086a3ce02a37c30e5b59762d0fd87f6c GIT binary patch literal 439 zcmV;o0Z9IdP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00A#aL_t(o!|j$)3W7isMIRRRQEnhwg_*g4$Q?wqpeTq| zQP8f+`nPsqGpM2COwOnm3cByp$2l_2ypib3;<5r}N)J{p4*#T>6-eQwc#tBG2PkF& zzLBC^Dqn(66rQBJ5+66LzW^`j(TL`>p$kfo#iAu$kpi9p9kBwQJz79?G7bQDceB6!HY0@g@p+c2#(Ng)E)Iuu$N26|$`L@X2{L7ua;qJ$Kq4*Jtc~ za6n!-w($0?3Oel5-Vm35pPkx(?fdZmXZNcDlK~P{o!VxK7V$h2LD4ckQA9{QkCQ4u h_)G!BCkiOOMqVK#%V*16@`wNc002ovPDHLkV1mtbt#tqZ literal 0 HcmV?d00001 diff --git a/static/img/ic_down_arrow@2x.png b/static/img/ic_down_arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb4fb4dc0ff4f975a8c40f0157aa6fcab2ed6d4 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8zNd?0h)3t!GlpCZ1_BHR7t|iG zxmdok_5DQ~&FqCNdM+LFT94-LHaHhK?W;-EpSPb&-kv#iETFdW#3c_y<|`UZf(}nNFRz0lQ`!Wl^a0VyDf4!&(3d#9>EBhxnfc4UW`u@) Q16s}C>FVdQ&MBb@0EB%&#{d8T literal 0 HcmV?d00001 diff --git a/static/img/ic_down_arrow_dark@2x.png b/static/img/ic_down_arrow_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3d7f83f64aafef0829be59c34c32c3d1c42fd3e4 GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8x~Gd{NCo5DGlqOjiW003JHM$E zxSU%mHt+aTj}3|0W|C2#{eQ>VE&99Yh3BJnM-Cgu*QJDsO38cgmF)4ni_Vdy+6vczRx)_H`njxg HN@xNAfk-$Z literal 0 HcmV?d00001 diff --git a/static/img/ic_edit@2x.png b/static/img/ic_edit@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a06bff5a2fc95d5117ce9880821384626d8864d GIT binary patch literal 239 zcmV+mP(MLbxIU@tZx11_<06E_Cm;EuJEI5KzuPNK$?!{-w< zW;{Mke9afh-**!m1^<@N@NHtL5K^$DhZ|FM=P=&qvIgiFq?I{~>Up@NR&` pI{_Y_4KVpkfXgQVHXn-(+!KC0r!M+UZ~XuO002ovPDHLkV1hXhU+Vw> literal 0 HcmV?d00001 diff --git a/static/img/ic_edit_dark@2x.png b/static/img/ic_edit_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..87f8de1ca302d83b746b391b83594f3cb0f39a4a GIT binary patch literal 222 zcmV<403rX0P)64_{qyg@{QSjgwU#qa z4m4P{;Lhvwjx;vjkO;gju?@H-u^UM2HOE<{iHGL)J?kWK5`Arw=$J_OB+)UI@Pi2f z-xB$l*f{u@(DEU%bn=GVhgbaVcl>4C{Ji+@`PrCx+&peR4_q}oZXP#}o5#%~;0>$Q Y-lS`>2u|CQasU7T07*qoM6N<$f(W@`r~m)} literal 0 HcmV?d00001 diff --git a/static/img/ic_font@2x.png b/static/img/ic_font@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..612d143869d6e916d10bb7bc7cdfef5c79dec2cb GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUfe86CCvq5QbAW383f~9Z3DW!eu2#JboFyt=akR{0P&O@ AdH?_b literal 0 HcmV?d00001 diff --git a/static/img/ic_font_dark@2x.png b/static/img/ic_font_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..44c3710ab159c47b96838a4f2f1d1b979a4ef623 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tQ%@JikP61P7Zv#!6nI!1|NYOr za6)yZ!pya6Bnn@9U3$mP;If5%2GhLb>22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/static/img/ic_info_dark@2x.png b/static/img/ic_info_dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b706f0d06a9a1e5e2f6606c01e84094ee6a99b42 GIT binary patch literal 640 zcmV-`0)PF9P)o#88fEO5na|OlO{3G z*yIPEVO{WsIjnKnVT>2DMzkql!ApMPMO=L<#3I9oI}LubOO+CNvScYxqQ)LKcQkDr zB9h}Xp02N~GG)+JIYyNnkc)PDZOnzu*#; zB8R%~1(i4Rl0>e>Eb#WLtxfOV(#vvo;8MrfX8# zOJwj-6bRC^RW#U^^>`Uk%GqUlp=9>%y-#&$qZ(w=qV2=#%SNueJg~}R_Q4VL}5`+@9~aM z91c`Iph*TZkfp^?^`Dp^0y)$Ij}vN48)8+LhbiruA|j3sO47|fb;`I*GKou>Ivq)^ z4u=V1QQ;ad;<{u73-W9;3T-}Qiy5qW$_D4g=4Y%kOJdS2(WFCs}CuNw zDb}81T+F&=$5fUxTa)j7b9Hb~IB=4Q$<$9wfPu;E_1?eoZZSeHftndSUHx3vIVCg! E05<#`uK)l5 literal 0 HcmV?d00001 diff --git a/static/img/ic_send@2x.png b/static/img/ic_send@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ef59e77678dbd3f5d866bca9058b6e90cb8d6098 GIT binary patch literal 344 zcmV-e0jK_nP)&W`22C;JryNwK41(MIA#GvFb#~s zwsao25E&6n03w(GL@)`A!D$_85` z%9!`-s3_@X0OI`y_5>OaJWhc7Ux1pxX-=Z&t1o;=Bq}NC$3*rSV}~5rah@Eqct#)^ z#sARKe>b^SMWD8;A|0im5z9_QQGnQ=9+)gN{ZC&+5rBvy01-u99ZM?OT$kUFh@ye? qUBLr0?$ZKzW0Q{J0Y5k327Cd{Pq+sr^Mm~W0000004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00AgTL_t(o!|j(nHiJPJg`a}-sYuHb;)wLI1e!$466lhs zk|j`wLY6?y;jjc+si+etc@<6yg3P=>-S^%F+?nT_x!)ZqrWn7uTC^z}77Wk{}Z>NtP0rWJv)`vNho% z2bd^zPb&?W7?cF8OqD&3IpdmVK2*SJ>A+Ulpur(Mu6X3lADCr-uXJozGG%|`JYb6k zo9uE#pCM1Y|7tK`I}gzo`u9p4IVZ`E7ZZ->I%E summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + return { + title: content.substring("# ".length, eol), + summary: summary, + }; + } + + var blankLine = content.indexOf("\n\n"); + if (blankLine !== -1 && blankLine <= eol && blankLine <= titleLen) { + // Title is in the format: + // + // Some title + // + // The body starts after that blank line above it. + var summary = content.substring(blankLine).trim(); + if (summary.length > summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + return { + title: content.substring(0, blankLine), + summary: summary, + }; + } + + // TODO: move this to the beginning + var title = content.trim(); + var summary = ""; + if (title.length > titleLen) { + // Content can't fit in the title, so figure out the summary + summary = title; + title = ""; + if (summary.length > summaryLen) { + summary = summary.substring(0, summaryLen) + "..."; + } + } else if (eol > 0) { + summary = title.substring(eol+1); + title = title.substring(0, eol); + } + return { + title: title, + summary: summary + }; + }; + + var post = getPostMeta(content); + post.id = id; + post.token = editToken; + post.created = created ? new Date(created) : new Date(); + post.client = "Pad"; + + return post; + }, + getTitleStrict: function(content) { + var eol = content.indexOf("\n"); + var title = ""; + var newContent = content; + if (content.indexOf("# ") === 0) { + // Title is in the format: + // # Some title + if (eol !== -1) { + // First line should start with # and end with \n + newContent = content.substring(eol).leftTrim(); + title = content.substring("# ".length, eol); + } + } + return { + title: title, + content: newContent + }; + }, +}; + +var He = { + create: function(name) { + return document.createElement(name); + }, + get: function(id) { + return document.getElementById(id); + }, + $: function(selector) { + var els = document.querySelectorAll(selector); + return els; + }, + postJSON: function(url, params, callback) { + var http = new XMLHttpRequest(); + + http.open("POST", url, true); + + // Send the proper header information along with the request + http.setRequestHeader("Content-type", "application/json"); + + http.onreadystatechange = function() { + if (http.readyState == 4) { + callback(http.status, JSON.parse(http.responseText)); + } + } + http.send(JSON.stringify(params)); + }, +}; + +String.prototype.leftTrim = function() { + return this.replace(/^\s+/,""); +}; diff --git a/templates/pad.tmpl b/templates/pad.tmpl new file mode 100644 index 0000000..cbce9bf --- /dev/null +++ b/templates/pad.tmpl @@ -0,0 +1,358 @@ +{{define "pad"}} + + + + {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} + + + + + + + + +
+ + + +
+
+

{{if .User}}{{else}}write.as{{end}} +

+ + + +
+ +
+ {{if .Editing}}{{end}} + +
+
+
+
+ + + + + +{{end}}