~danilax86: 1 Add bookmarklet 9 files changed, 168 insertions(+), 2 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~bouncepaw/betula/patches/42820/mbox | git am -3Learn more about email & git
From: Danila Gorelko <danila@danilax86.space> Thanks to Umar, https://handlerug.me/betula-save-bookmarklet Fixes: https://todo.sr.ht/~bouncepaw/betula/59 --- cmd/betula/main.go | 1 + web/bookmarklet.go | 20 ++++++++++ web/bookmarklet.gohtml | 20 ++++++++++ web/bookmarklet.js | 91 ++++++++++++++++++++++++++++++++++++++++++ web/handlers.go | 18 +++++++++ web/settings.gohtml | 2 +- web/skeleton.gohtml | 5 ++- web/style.css | 12 ++++++ web/templates.go | 1 + 9 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 web/bookmarklet.go create mode 100644 web/bookmarklet.gohtml create mode 100644 web/bookmarklet.js diff --git a/cmd/betula/main.go b/cmd/betula/main.go index c536fa9..5b2d798 100644 --- a/cmd/betula/main.go +++ b/cmd/betula/main.go @@ -41,5 +41,6 @@ func main() { settings.WritePort(port) } settings.Index() + web.BookmarkletScriptGenerate(settings.SiteURL()) web.StartServer() } diff --git a/web/bookmarklet.go b/web/bookmarklet.go new file mode 100644 index 0000000..8f69caa --- /dev/null +++ b/web/bookmarklet.go @@ -0,0 +1,20 @@ +package web + +import ( + "fmt" + "log" +) + +var BookmarkletScript string = "" + +func BookmarkletScriptGenerate(siteUrl string) { + BookmarkletScript = bookmarkletScript(siteUrl) +} + +func bookmarkletScript(siteUrl string) string { + raw, err := fs.ReadFile("bookmarklet.js") + if err != nil { + log.Fatalln(err) + } + return fmt.Sprintf(string(raw), siteUrl) +} diff --git a/web/bookmarklet.gohtml b/web/bookmarklet.gohtml new file mode 100644 index 0000000..5b24386 --- /dev/null +++ b/web/bookmarklet.gohtml @@ -0,0 +1,20 @@ +{{define "title"}}Bookmarklet{{end}} +{{define "body"}} + <main> + <article> + <h2>Bookmarklet</h2> + <p> + This special link allows you to add a link to your betula directly by using a bookmark in your web + browser. + </p> + <div class="bookmarklet"> + <a href="javascript:{{.Script}}"> + Add to Betula + </a> + </div> + <p> + Drag and drop this link to your bookmarks. + </p> + </article> + </main> +{{end}} diff --git a/web/bookmarklet.js b/web/bookmarklet.js new file mode 100644 index 0000000..08f4b84 --- /dev/null +++ b/web/bookmarklet.js @@ -0,0 +1,91 @@ +// Save link bookmarklet for Betula +// 2023 Umar Getagazov <umar@handlerug.me> +// Public domain, but attribution appreciated. +// https://handlerug.me/betula-save-bookmarklet + +(($) => { + function getSelectionInMycomarkup() { + function convert(node, parentNodeName = '') { + if (node instanceof Text) { + if (node.textContent.trim() === '') { + return ''; + } + + return node.textContent + .replace(/\\/g, '\\\\') + .replace(/\*\*/g, '\\**') + .replace(/\/\//g, '\\//') + .replace(/\+\+/g, '\\++'); + } + + let nodeName = node.nodeName.toLowerCase(); + + let result = ''; + for (const child of node.childNodes) { + result += convert(child, nodeName); + } + + if (nodeName === 'p') { + return `\n\n${result.trim()}\n\n`; + } else if (nodeName === 'br') { + return '\n'; + } else if (nodeName === 'a') { + return `[[${decodeURI(node.href)} | ${result}]]`; + } else if (nodeName === 'b' || nodeName === 'strong') { + return `**${result}**`; + } else if (nodeName === 'i' || nodeName === 'em') { + return `//${result}//`; + } else if (nodeName === 'h1') { + return `\n\n${result}\n\n`; + } else if (nodeName === 'h2') { + return `= ${result}\n\n`; + } else if (nodeName === 'h3') { + return `== ${result}\n\n`; + } else if (nodeName === 'h4') { + return `=== ${result}\n\n`; + } else if (nodeName === 'h5') { + return `==== ${result}\n\n`; + } else if (nodeName === 'li') { + if (node.children.length === 1) { + let link = node.children[0]; + if (link.nodeName.toLowerCase() === 'a') { + if (link.href === link.innerText || decodeURI(link.href) === link.innerText) { + return `=> ${decodeURI(link.href)}\n`; + } else { + return `=> ${decodeURI(link.href)} | ${link.innerText}\n`; + } + } + } + return parentNodeName === 'ol' + ? `*. ${result}\n` + : `* ${result}\n`; + } else { + return result; + } + } + + let selection = window.getSelection(); + if (selection.rangeCount === 0) { + return ''; + } + let range = selection.getRangeAt(0); + let contents = range.cloneContents(); + return convert(contents).replace(/\n\n+/g, '\n\n'); + } + + let u = '%s/save-link?' + new URLSearchParams({ + url: ($('link[rel=canonical]') || location).href, + title: $('meta[property="og:title"]')?.content || document.title, + description: ( + getSelectionInMycomarkup() || + $('meta[property="og:description"]')?.content || + $('meta[name=description]')?.content + )?.trim().replace(/^/gm, '> ') || '' + }); + + try { + window.open(u, '_blank', 'location=yes,width=600,height=800,scrollbars=yes,status=yes,noopener,noreferrer'); + } catch { + location.href = u; + } +})(document.querySelector.bind(document)); diff --git a/web/handlers.go b/web/handlers.go index 50a3a9c..3c6626c 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -66,10 +66,23 @@ func init() { mux.HandleFunc("/login", handlerLogin) mux.HandleFunc("/logout", handlerLogout) mux.HandleFunc("/settings", adminOnly(handlerSettings)) + mux.HandleFunc("/bookmarklet", adminOnly(handlerBookmarklet)) mux.HandleFunc("/static/style.css", handlerStyle) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(fs)))) } +type dataBookmarklet struct { + *dataCommon + Script string +} + +func handlerBookmarklet(w http.ResponseWriter, rq *http.Request) { + templateExec(w, templateBookmarklet, dataBookmarklet{ + dataCommon: emptyCommon(), + Script: BookmarkletScript, + }, rq) +} + func handlerHelp(w http.ResponseWriter, rq *http.Request) { http.Redirect(w, rq, "/help/en/index", http.StatusSeeOther) } @@ -270,6 +283,11 @@ func handlerSettings(w http.ResponseWriter, rq *http.Request) { CustomCSS: rq.FormValue("custom-css"), } + // New bookmarklet script on SiteURL changes + if newSettings.SiteURL != settings.SiteURL() { + BookmarkletScriptGenerate(newSettings.SiteURL) + } + // If the port ≤ 0 or not really numeric, show error. if port, err := strconv.Atoi(rq.FormValue("network-port")); err != nil || port <= 0 { newSettings.NetworkPort = uint(port) diff --git a/web/settings.gohtml b/web/settings.gohtml index c692111..27b6964 100644 --- a/web/settings.gohtml +++ b/web/settings.gohtml @@ -18,7 +18,7 @@ <p class="input-caption"> The address at which your Betula is hosted. Type out the protocol (http or https). - This information is used for RSS feed generation.</p> + This information is used for RSS feed and bookmarklet generation.</p> </div> <div> diff --git a/web/skeleton.gohtml b/web/skeleton.gohtml index 6576546..04c9cd0 100644 --- a/web/skeleton.gohtml +++ b/web/skeleton.gohtml @@ -31,8 +31,11 @@ <li> <a href="/digest-rss">Site RSS</a> </li> + {{if .Authorized}} + <li><a href="/bookmarklet">Bookmarklet</a></li> + {{end}} </ul> </nav> {{template "body" .}} </body> -</html> \ No newline at end of file +</html> diff --git a/web/style.css b/web/style.css index 47bd7ae..eafeb9b 100644 --- a/web/style.css +++ b/web/style.css @@ -391,6 +391,18 @@ a.btn_destructive:visited, color: white; } +.bookmarklet { + border: 1px dashed #999; + border-radius: 5px; + padding: 15px; + text-align: center; +} + +.bookmarklet a { + text-decoration: none; + font-weight: bold; +} + @media (prefers-color-scheme: dark) { .btn { border: #444 solid 1px; diff --git a/web/templates.go b/web/templates.go index c2633ed..5b00c27 100644 --- a/web/templates.go +++ b/web/templates.go @@ -68,6 +68,7 @@ var templateDay = templateFrom(funcMapForPosts, "post-fragment", "day") var templateEditTag = templateFrom(funcMapForForm, "edit-tag") var templateHelp = templateFrom(nil, "help") var templateAbout = templateFrom(funcMapForTime, "about") +var templateBookmarklet = templateFrom(nil, "bookmarklet") var funcMapForPosts = template.FuncMap{ "randomGlobe": func() string { -- 2.38.5
builds.sr.ht <builds@sr.ht>betula/patches/.build.yml: SUCCESS in 2m12s [Add bookmarklet][0] v2 from [~danilax86][1] [0]: https://lists.sr.ht/~bouncepaw/betula/patches/42820 [1]: mailto:danila@danilax86.space ✓ #1026391 SUCCESS betula/patches/.build.yml https://builds.sr.ht/~bouncepaw/job/1026391
Oh, I think it can be done even simpler. We don't really need `BookmarkletScriptGenerate`. Another function which we have to not forget to call when the time is right? It's better to avoid it. I think substitution the URL of the site can be done in the handler, like this: func handlerBookmarklet(w http.ResponseWriter, rq *http.Request) { templateExec(w, templateBookmarklet, dataBookmarklet{ dataCommon: emptyCommon(), Script: fmt.Sprintf(BookmarkletScript, settings.SiteURL()), }, rq) } But how do we read the source script? In the `web` package, where you put `bookmarklet.go`, there is the `fs` variable available (see `handlers.go`), from which we can read the file. Better yet, we can declare `BookmarkletScript` like that: //go:embed bookmarklet.js var BookmarkletScript string Also, does it really need to be exported? Please look into that. Also, since we won't need `BookmarkletScriptGenerate` with my approach, we won't need `bookmarklet.go`.