improve outline UX; add print mode and reading time estimation
This commit is contained in:
@@ -7,16 +7,15 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/everywall/ladder/proxychain"
|
||||
|
||||
"github.com/markusmobius/go-trafilatura"
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
|
||||
//"github.com/go-shiori/dom"
|
||||
"github.com/markusmobius/go-trafilatura"
|
||||
)
|
||||
|
||||
//go:embed vendor/generate_readable_outline.html
|
||||
@@ -63,19 +62,24 @@ func GenerateReadableOutline() proxychain.ResponseModification {
|
||||
html.Render(&b, extract.ContentNode)
|
||||
distilledHTML := b.String()
|
||||
|
||||
siteName := strings.Split(extract.Metadata.Sitename, ";")[0]
|
||||
title := strings.Split(extract.Metadata.Title, "|")[0]
|
||||
fmtDate := createWikipediaDateLink(extract.Metadata.Date)
|
||||
readingTime := formatDuration(estimateReadingTime(extract.ContentText))
|
||||
|
||||
// populate template parameters
|
||||
data := map[string]interface{}{
|
||||
"Success": true,
|
||||
"Image": extract.Metadata.Image,
|
||||
"Description": extract.Metadata.Description,
|
||||
"Sitename": strings.Split(extract.Metadata.Sitename, ";")[0],
|
||||
"Sitename": siteName,
|
||||
"Hostname": extract.Metadata.Hostname,
|
||||
"Url": "/" + chain.Request.URL.String(),
|
||||
"Title": extract.Metadata.Title, // todo: modify CreateReadableDocument so we don't have <h1> titles duplicated?
|
||||
"Date": extract.Metadata.Date.String(),
|
||||
"Author": createWikipediaSearchLinks(extract.Metadata.Author),
|
||||
//"Author": extract.Metadata.Author,
|
||||
"Body": distilledHTML,
|
||||
"Title": title,
|
||||
"Date": fmtDate,
|
||||
"Author": createDDGFeelingLuckyLinks(extract.Metadata.Author, extract.Metadata.Hostname),
|
||||
"Body": distilledHTML,
|
||||
"ReadingTime": readingTime,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -157,9 +161,20 @@ func rewriteHrefLinks(n *html.Node, baseURL string, apiPath string) {
|
||||
recurse(n)
|
||||
}
|
||||
|
||||
// createWikipediaSearchLinks takes in comma or semicolon separated terms,
|
||||
// then turns them into <a> links searching for the term.
|
||||
func createWikipediaSearchLinks(searchTerms string) string {
|
||||
// createWikipediaDateLink takes in a date
|
||||
// and returns an <a> link pointing to the current events page for that day
|
||||
func createWikipediaDateLink(t time.Time) string {
|
||||
url := fmt.Sprintf("https://en.wikipedia.org/wiki/Portal:Current_events#%s", t.Format("2006_January_2"))
|
||||
date := t.Format("January 2, 2006")
|
||||
return fmt.Sprintf("<a rel=\"noreferrer\" href=\"%s\">%s</a>", url, date)
|
||||
}
|
||||
|
||||
// createDDGFeelingLuckyLinks takes in comma or semicolon separated terms,
|
||||
// then turns them into <a> links searching for the term using DuckDuckGo's I'm
|
||||
// feeling lucky feature. It will redirect the user immediately to the first search result.
|
||||
func createDDGFeelingLuckyLinks(searchTerms string, siteHostname string) string {
|
||||
|
||||
siteHostname = strings.TrimSpace(siteHostname)
|
||||
semiColonSplit := strings.Split(searchTerms, ";")
|
||||
|
||||
var links []string
|
||||
@@ -171,11 +186,13 @@ func createWikipediaSearchLinks(searchTerms string) string {
|
||||
continue
|
||||
}
|
||||
|
||||
encodedTerm := url.QueryEscape(trimmedTerm)
|
||||
ddgQuery := fmt.Sprintf(` site:%s intitle:"%s"`, strings.TrimPrefix(siteHostname, "www."), trimmedTerm)
|
||||
|
||||
wikiURL := fmt.Sprintf("https://en.wikipedia.org/w/index.php?search=%s", encodedTerm)
|
||||
encodedTerm := `\%s:` + url.QueryEscape(ddgQuery)
|
||||
//ddgURL := `https://html.duckduckgo.com/html/?q=` + encodedTerm
|
||||
ddgURL := `https://www.duckduckgo.com/?q=` + encodedTerm
|
||||
|
||||
link := fmt.Sprintf("<a href=\"%s\">%s</a>", wikiURL, trimmedTerm)
|
||||
link := fmt.Sprintf("<a rel=\"noreferrer\" href=\"%s\">%s</a>", ddgURL, trimmedTerm)
|
||||
links = append(links, link)
|
||||
}
|
||||
|
||||
@@ -187,3 +204,66 @@ func createWikipediaSearchLinks(searchTerms string) string {
|
||||
|
||||
return strings.Join(links, " ")
|
||||
}
|
||||
|
||||
// estimateReadingTime estimates how long the given text will take to read using the given configuration.
|
||||
func estimateReadingTime(text string) time.Duration {
|
||||
if len(text) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Init options with default values.
|
||||
WordsPerMinute := 200
|
||||
WordBound := func(b byte) bool {
|
||||
return b == ' ' || b == '\n' || b == '\r' || b == '\t'
|
||||
}
|
||||
|
||||
words := 0
|
||||
start := 0
|
||||
end := len(text) - 1
|
||||
|
||||
// Fetch bounds.
|
||||
for WordBound(text[start]) {
|
||||
start++
|
||||
}
|
||||
for WordBound(text[end]) {
|
||||
end--
|
||||
}
|
||||
|
||||
// Calculate the number of words.
|
||||
for i := start; i <= end; {
|
||||
for i <= end && !WordBound(text[i]) {
|
||||
i++
|
||||
}
|
||||
|
||||
words++
|
||||
|
||||
for i <= end && WordBound(text[i]) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Reading time stats.
|
||||
minutes := math.Ceil(float64(words) / float64(WordsPerMinute))
|
||||
duration := time.Duration(math.Ceil(minutes) * float64(time.Minute))
|
||||
|
||||
return duration
|
||||
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
// Check if the duration is less than one minute
|
||||
if d < time.Minute {
|
||||
seconds := int(d.Seconds())
|
||||
return fmt.Sprintf("%d seconds", seconds)
|
||||
}
|
||||
|
||||
// Convert the duration to minutes
|
||||
minutes := int(d.Minutes())
|
||||
|
||||
// Format the string for one or more minutes
|
||||
if minutes == 1 {
|
||||
return "1 minute"
|
||||
} else {
|
||||
return fmt.Sprintf("%d minutes", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512"
|
||||
class="h-8 focus:outline-none focus:ring focus:border-[#7AA7D1] ring-offset-2"
|
||||
class="noprint h-8 focus:outline-none focus:ring focus:border-[#7AA7D1] ring-offset-2"
|
||||
>
|
||||
<path
|
||||
fill="#7AA7D1"
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center z-10">
|
||||
<div class="noprint flex justify-center z-10">
|
||||
<div class="relative" id="dropdown">
|
||||
<button
|
||||
aria-expanded="closed"
|
||||
@@ -92,6 +92,15 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-expanded="closed"
|
||||
onclick="document.getElementById('readingtime').innerText = 'Date Accessed: '+(new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })); [...document.querySelectorAll('.noprint')].forEach(e => e.remove()); window.print()"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-full h-12 px-4 py-2 text-sm font-medium text-slate-600 dark:text-slate-400 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-white dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-700 hover:text-slate-500 dark:hover:text-slate-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="16" width="16" viewBox="0 0 512 512"><path d="M128 0C92.7 0 64 28.7 64 64v96h64V64H354.7L384 93.3V160h64V93.3c0-17-6.7-33.3-18.7-45.3L400 18.7C388 6.7 371.7 0 354.7 0H128zM384 352v32 64H128V384 368 352H384zm64 32h32c17.7 0 32-14.3 32-32V256c0-35.3-28.7-64-64-64H64c-35.3 0-64 28.7-64 64v96c0 17.7 14.3 32 32 32H64v64c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V384zM432 248a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="dropdown_panel"
|
||||
class="hidden absolute right-0 mt-2 w-52 rounded-md bg-white dark:bg-slate-900 shadow-md border border-slate-400 dark:border-slate-700"
|
||||
@@ -326,24 +335,28 @@
|
||||
{{.Error}}
|
||||
</code>
|
||||
{{else}}
|
||||
<div class="flex flex-col gap-1 mt-3">
|
||||
<h1>
|
||||
<a href="{{.Url}}" class="text-slate-900 dark:text-slate-200"> {{.Title}} </a>
|
||||
</h1>
|
||||
{{if ne .Date ""}}
|
||||
<small
|
||||
class="text-sm font-medium leading-none text-slate-600 dark:text-slate-400"
|
||||
>{{.Date}}</small
|
||||
>
|
||||
{{end}}
|
||||
{{if ne .Author ""}}
|
||||
<small
|
||||
class="text-sm font-medium leading-none text-slate-600 dark:text-slate-400"
|
||||
>{{.Author}}</small
|
||||
>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
<a href="{{.Url}}" class="text-slate-900 dark:text-slate-200"> {{.Title}} </a>
|
||||
</h2>
|
||||
|
||||
<div class="flex justify-between items-center gap-1 mt-3">
|
||||
|
||||
|
||||
<div>
|
||||
{{if ne .Author ""}}
|
||||
<small class="text-sm font-medium leading-none text-slate-600 dark:text-slate-400">{{.Author}} | </small>
|
||||
{{end}}
|
||||
|
||||
{{if ne .Date ""}}
|
||||
<small class="text-sm font-medium leading-none text-slate-600 dark:text-slate-400">{{.Date}}</small>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<small id="readingtime" class="text-sm font-medium leading-none text-slate-600 dark:text-slate-400">Reading Time: {{.ReadingTime}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
@@ -357,21 +370,22 @@
|
||||
</main>
|
||||
|
||||
<div class="my-2"></div>
|
||||
<footer class="mx-4 text-center text-slate-600 dark:text-slate-400">
|
||||
<p>
|
||||
Code Licensed Under GPL v3.0 |
|
||||
<a
|
||||
href="https://github.com/everywall/ladder"
|
||||
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
|
||||
>View Source</a
|
||||
>
|
||||
|
|
||||
<footer class="noprint mx-4 my-2 pt-2 border-t border-gray-300 dark:border-gray-700 text-center text-slate-600 dark:text-slate-400">
|
||||
|
||||
<small>
|
||||
<a
|
||||
href="https://github.com/everywall"
|
||||
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
|
||||
>Everywall</a
|
||||
>
|
||||
</p>
|
||||
|
|
||||
<a
|
||||
href="https://github.com/everywall/ladder"
|
||||
class="hover:text-blue-500 dark:hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300"
|
||||
>Source</a
|
||||
>
|
||||
| Code Licensed Under GPL v3.0
|
||||
</small>
|
||||
</footer>
|
||||
<div class="my-2"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user