~bouncepaw/betula

betula: Add bookmarklet v2 SUPERSEDED

~danilax86: 1
 Add bookmarklet

 9 files changed, 168 insertions(+), 2 deletions(-)
#1026391 .build.yml success
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH betula v2] Add bookmarklet Export this patch

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
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`.