bm-html (3948 bytes)
1 #!/bin/sh 2 3 # bm-html: print the bookmarks as one self-contained web page. 4 # 5 # bm-html > ~/public_html/bookmarks.html 6 # bm --tag code --list | bm-html -t 'Code' - > code.html 7 # 8 # Without a file the default bookmark file is read; "-" is stdin. 9 # 10 # The page lists the bookmarks under each of their tags, with links to the 11 # tags at the top. A few lines of script add a filter box; without script the 12 # page is still the complete list. page.html?q=words starts out filtered, so 13 # the page can serve as a search engine keyword in a browser, or in bm's own 14 # engines file: b | https://example.org/bookmarks.html?q=%s 15 16 usage () { 17 printf 'usage: bm-html [-t title] [file | -] ...\n' 18 } 19 20 title=Bookmarks 21 while [ $# -gt 0 ]; do 22 case $1 in 23 -h|--help) usage; exit 0 ;; 24 -t) 25 [ $# -ge 2 ] || { usage >&2; exit 2; } 26 title=$2 27 shift 28 ;; 29 --) shift; break ;; 30 -) break ;; 31 -?*) usage >&2; exit 2 ;; 32 *) break ;; 33 esac 34 shift 35 done 36 [ $# -gt 0 ] || set -- "${BOOKMARKS:-${XDG_DATA_HOME:-$HOME/.local/share}/sbm/bookmarks}" 37 38 cat -- "$@" | LC_ALL=C sort -f -t' ' -k2,2 -k1,1 | SBM_TITLE=$title awk -F'\t' ' 39 function esc(s) { 40 gsub(/&/, "\\&", s); gsub(/</, "\\<", s); gsub(/>/, "\\>", s) 41 gsub(/"/, "\\"", s) 42 return s 43 } 44 function host(u) { 45 sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "", u) 46 sub(/[\/?#].*/, "", u) 47 return u 48 } 49 NF == 0 || /^#/ { next } 50 # Never publish a link that would run script. 51 tolower($1) ~ /^[ \t]*(javascript|data|vbscript):/ { next } 52 { 53 total++ 54 item = sprintf("<li><a href=\"%s\">%s</a> <small>%s</small>", esc($1), \ 55 esc($2 == "" ? $1 : $2), esc(host($1))) 56 n = split($3, tag, " ") 57 for (i = 1; i <= n; i++) 58 item = item sprintf(" <a class=t href=\"#%s\">%s</a>", esc(tag[i]), esc(tag[i])) 59 if (n == 0) { n = 1; tag[1] = "" } 60 for (i = 1; i <= n; i++) { 61 if (!(tag[i] in items)) names[++count] = tag[i] 62 items[tag[i]] = items[tag[i]] item "\n" 63 } 64 } 65 END { 66 # Insertion sort: tags by name, the untagged last. 67 for (i = 2; i <= count; i++) { 68 name = names[i] 69 for (j = i - 1; j > 0 && (names[j] == "" || (name != "" && names[j] > name)); j--) 70 names[j + 1] = names[j] 71 names[j + 1] = name 72 } 73 t = esc(ENVIRON["SBM_TITLE"]) 74 print "<!doctype html>" 75 print "<meta charset=utf-8>" 76 print "<meta name=viewport content=\"width=device-width, initial-scale=1\">" 77 print "<title>" t "</title>" 78 print "<style>" 79 print "body{max-width:50em;margin:1em auto;padding:0 1em;font:1em/1.5 sans-serif}" 80 print "#q{width:100%;box-sizing:border-box;font:inherit;padding:.3em}" 81 print "ul{padding-left:1.2em}small,.t{color:#777;font-size:.85em}.t{margin-left:.3em}" 82 print "</style>" 83 print "<h1>" t "</h1>" 84 print "<input id=q type=search placeholder=\"filter " total " bookmarks\" hidden>" 85 printf "<p>" 86 for (i = 1; i <= count; i++) 87 if (names[i] != "") 88 printf "<a href=\"#%s\">%s</a> ", esc(names[i]), esc(names[i]) 89 print "</p>" 90 for (i = 1; i <= count; i++) { 91 name = names[i] == "" ? "untagged" : names[i] 92 printf "<section><h2 id=\"%s\">%s</h2>\n<ul>\n%s</ul></section>\n", \ 93 esc(name), esc(name), items[names[i]] 94 } 95 print "<script>" 96 print "var q=document.getElementById(\"q\");q.hidden=false;" 97 print "function f(){var w=q.value.toLowerCase().split(/\\s+/);" 98 print "document.querySelectorAll(\"section\").forEach(function(s){var n=0;" 99 print "s.querySelectorAll(\"li\").forEach(function(l){" 100 print "var t=(l.textContent+\" \"+l.firstChild.href).toLowerCase();" 101 print "var ok=w.every(function(x){return t.indexOf(x)>=0});l.hidden=!ok;if(ok)n++});" 102 print "s.hidden=!n})}" 103 print "q.oninput=f;q.value=new URLSearchParams(location.search).get(\"q\")||\"\";f();" 104 print "</script>" 105 }'