commit 577676dddee56219928f0ee7af6885546820992f Spenser Truex <truex@equwal.com> 2026-09-22 20:11:19 -0700 bm-page: the text of the page in the fzf preview The new tool bm-page prints a web page as plain text: its title, then its main part (the <main> element, when the page has one), without scripts, styles, comments and svg. It fetches the page with curl (5 second limit, at most 512k) and removes control characters, so that a page cannot send escape sequences to the terminal. SBM_FETCH=0 keeps it off the network, as for bm-title. The fzf preview of bm now shows the bookmark in full, then the text of its page when bm-page is installed. The preview is on the right, half of the width, because a page needs more room than five lines. A short sleep before the fetch keeps a fast scroll from fetching each page on the way. make install, the Windows installer and the package files include bm-page.
Makefile | 2 +- README | 16 +- bm | 9 +- bm-page | 212 +++++++++++++++++++++ config.mk | 3 +- contrib/windows/sbm.nsi | 4 +- packaging/README.md | 4 +- packaging/chocolatey/tools/chocolateyInstall.ps1 | 2 +- packaging/chocolatey/tools/chocolateyUninstall.ps1 | 2 +- packaging/freebsd/deskutils/sbm/pkg-plist | 1 + packaging/rpm/sbm.spec | 1 + packaging/scoop/sbm.json | 3 +- test/run.sh | 79 +++++++- 13 files changed, 318 insertions(+), 20 deletions(-)
diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 7ae6985..d1d4764 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ include config.mk -SCRIPTS = bm bm-migrate bm-import bm-check bm-html bm-title bm-commit bm-watch bm-sync +SCRIPTS = bm bm-migrate bm-import bm-check bm-html bm-title bm-page bm-commit bm-watch bm-sync TESTS = test/run.sh test/fakemenu test/fakefzf all: diff --git a/README b/README old mode 100644 new mode 100755 index d39c3f9..f218327 --- a/README +++ b/README @@ -22,6 +22,7 @@ other Unix tools, and you install only the ones you use: bm-html bookmark lines -> a searchable web page bm-migrate the old sbm file format -> bookmark lines bm-title a URL -> the page's title + bm-page a URL -> the page as text bm-commit commit the bookmark file to git bm-watch browser bookmarks -> bm, now and after each change bm-sync the bookmark file <-> your other devices @@ -38,8 +39,8 @@ date +%s. == Requirements == dmenu (or fzf), one of xclip, xsel or wl-clipboard, sh, awk, make. -curl for bm-title, bm-check and bm-sync, jq for bm-import of Chromium files, -git for bm-commit. +curl for bm-title, bm-page, bm-check and bm-sync, jq for bm-import of +Chromium files, git for bm-commit. == Install == Edit TOOLS in config.mk to choose what gets installed, then @@ -98,7 +99,10 @@ Closing the second menu goes back to the list, closing the list leaves. [add] at the top of the list adds a bookmark. With fzf (no display, or SBM_MENU=fzf) it is one screen: the bookmark under -the cursor is previewed live, and keys do the rest. +the cursor is previewed live, and keys do the rest. With bm-page installed, +the preview also shows the text of the page: its title, then its main part. +bm-page fetches the page with curl (5 second limit, at most 512k; SBM_FETCH=0 +turns it off). ENTER open ctrl-a add ctrl-s next sort order ctrl-y copy ctrl-d delete (asks) @@ -275,9 +279,9 @@ version 0.11 from 2016, and it has no --preview and no --expect. The wrapper SBM_OPEN, SBM_COPY, SBM_PASTE and SBM_FZF_OPTS for Cygwin. Settings in ~/.bash_profile win. -For bm-title, bm-check and bm-import, install the curl and jq packages of -Cygwin. The Windows builds of curl and jq do not understand Cygwin paths, -such as /dev/null. To remove sbm, use Apps in the Windows settings. The +For bm-title, bm-page, bm-check and bm-import, install the curl and jq +packages of Cygwin. The Windows builds of curl and jq do not understand +Cygwin paths, such as /dev/null. To remove sbm, use Apps in the Windows settings. The uninstaller keeps your bookmarks. contrib/windows/build-installer.sh makes the installer with NSIS. diff --git a/bm b/bm index af6345d..873d658 100755 --- a/bm +++ b/bm @@ -235,8 +235,11 @@ is_bookmark () { } # For fzf a fifth field is added, and it is all that is listed. The first -# four feed the preview, which follows the cursor. -PREVIEW="printf '%s\n%s\ntags: %s\nused: %s\n' {1} {2} {3} {4}" +# four feed the preview, which follows the cursor: the bookmark in full, then +# the text of its page when bm-page is installed. fzf stops the preview when +# the cursor moves on, so the short sleep keeps a fast scroll from fetching +# each page on the way. A sleep that knows no fractions fails and skips it. +PREVIEW="printf '%s\n%s\ntags: %s\nused: %s\n\n' {1} {2} {3} {4}; sleep 0.3 2>/dev/null; bm-page {1} 2>/dev/null" fzfrows () { awk -F'\t' 'NF { desc = $2 @@ -248,7 +251,7 @@ fzfrows () { # withpreview <fzf command and options>: for the output of fzfrows. withpreview () { "$@" --delimiter="$TAB" --with-nth=5.. \ - --preview-window=down:5:wrap --preview="$PREVIEW" + --preview-window=right:50%:wrap --preview="$PREVIEW" } # choose <prompt>: a bookmark's URL, or whatever else the user typed. diff --git a/bm-page b/bm-page new file mode 100755 index 0000000..41d73ce --- /dev/null +++ b/bm-page @@ -0,0 +1,212 @@ +#!/bin/sh + +# bm-page: print a web page as plain text: its title, a blank line, then its +# text. When it is installed, the fzf list of bm shows the page of the +# bookmark under the cursor in the preview, live. SBM_FETCH=0 keeps it off +# the network. +# +# bm-page https://suckless.org/ | less +# +# Only http and https pages are fetched. curl stops after 5 seconds, and dd +# passes at most 512k of the page. Scripts, styles and comments are left +# out. Control characters are removed, so that a page cannot send escape +# sequences to the terminal. + +if [ $# -ne 1 ]; then + printf 'usage: bm-page <url>\n' >&2 + exit 2 +fi +[ "${SBM_FETCH:-1}" != 0 ] || exit 0 +case $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') in + http://*|https://*) ;; + *) exit 0 ;; +esac + +# The first awk reads the page as one record (RS is a control character), +# then splits it at each "<". Each part starts with a tag and ends with the +# text after it, so one pass through the parts reads the page in linear +# time. The second awk tidies the white space. +curl -sL --max-time 5 -A 'Mozilla/5.0' -- "$1" 2>/dev/null \ + | dd bs=1024 count=512 2>/dev/null | LC_ALL=C awk ' + function utf8(c) { + if (c == 160) return " " + if (c < 128) return sprintf("%c", c) + if (c < 2048) return sprintf("%c%c", 192 + int(c / 64), 128 + c % 64) + if (c < 65536) + return sprintf("%c%c%c", 224 + int(c / 4096), 128 + int(c / 64) % 64, 128 + c % 64) + if (c < 1114112) + return sprintf("%c%c%c%c", 240 + int(c / 262144), 128 + int(c / 4096) % 64, + 128 + int(c / 64) % 64, 128 + c % 64) + return "" + } + function entity(name, n, i) { + if (name ~ /^#[0-9]+$/) return utf8(substr(name, 2) + 0) + if (name ~ /^#[xX][0-9A-Fa-f]+$/) { + n = 0 + for (i = 3; i <= length(name); i++) + n = n * 16 + index("0123456789abcdef", tolower(substr(name, i, 1))) - 1 + return utf8(n) + } + if (name in named) return named[name] + return "&" name ";" + } + function decode(s, out, i, j, name) { + out = "" + while ((i = index(s, "&")) > 0) { + out = out substr(s, 1, i - 1) + s = substr(s, i + 1) + j = index(s, ";") + name = substr(s, 1, j - 1) + if (j > 1 && j <= 12 && name ~ /^(#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*)$/) { + out = out entity(name) + s = substr(s, j + 1) + } else { + out = out "&" + } + } + return out s + } + # text: the text of the page, or of its title. As in a browser, a line + # break in the text is a space. The line breaks come from the tags: brk + # tells how many are waiting for the next text. + function text(s) { + if (intitle) { + title = title " " s + return + } + s = decode(s) + gsub(/[ \t\r\n]+/, " ", s) + if (s == "" || s == " ") { + if (s == " " && !brk) printf " " + return + } + if (brk) { + printf "%s", brk == 2 ? "\n\n" : "\n" + brk = 0 + sub(/^ /, "", s) + } + printf "%s", s + } + BEGIN { + RS = "\001" + named["amp"] = "&"; named["lt"] = "<"; named["gt"] = ">"; named["quot"] = "\"" + named["apos"] = "\047"; named["nbsp"] = " " + named["ndash"] = utf8(8211); named["mdash"] = utf8(8212); named["hellip"] = utf8(8230) + named["lsquo"] = utf8(8216); named["rsquo"] = utf8(8217) + named["ldquo"] = utf8(8220); named["rdquo"] = utf8(8221) + named["laquo"] = utf8(171); named["raquo"] = utf8(187); named["middot"] = utf8(183) + named["bull"] = utf8(8226); named["copy"] = utf8(169); named["reg"] = utf8(174) + named["trade"] = utf8(8482); named["deg"] = utf8(176); named["times"] = utf8(215) + named["euro"] = utf8(8364) + # Tags that start a line, and tags that start a paragraph. + split("br div li tr dt dd header footer nav main section article aside " \ + "figure figcaption form details summary address hr", b, " ") + for (i in b) line[b[i]] = 1 + split("p h1 h2 h3 h4 h5 h6 ul ol dl table blockquote pre", b, " ") + for (i in b) para[b[i]] = 1 + # Tags whose content is no text of the page. + split("script style noscript template svg iframe object", b, " ") + for (i in b) hidden[b[i]] = 1 + } + { page = page $0 } + END { + if (substr(page, 1, 5) == "%PDF-") { + print "(a PDF document)" + exit + } + n = split(page, part, "<") + text(part[1]) + for (i = 2; i <= n; i++) { + p = part[i] + if (comment || substr(p, 1, 3) == "!--") { + k = index(p, "-->") + comment = !k + if (k && skip == "") text(substr(p, k + 3)) + continue + } + k = index(p, ">") + if (!k) { + # A "<" in the text, or the end of a page that dd cut off. + if (skip == "" && i < n) text("<" p) + continue + } + tag = tolower(substr(p, 1, k - 1)) + closing = substr(tag, 1, 1) == "/" + name = tag + sub(/^\//, "", name) + sub(/[^a-z0-9].*/, "", name) + rest = substr(p, k + 1) + if (skip != "") { + if (closing && name == skip) { + skip = "" + text(rest) + } + continue + } + if (name in hidden && !closing && substr(tag, length(tag)) != "/") { + skip = name + continue + } + if (name == "title" && !done) { + if (closing) { + intitle = 0 + done = 1 + t = decode(title) + gsub(/[ \t\r\n]+/, " ", t) + printf "\n\001T%s\n", t + brk = 0 + } else { + intitle = 1 + } + } else if (name == "main") { + # Marks for the second awk: the main part of the page. + printf "\n\001%s\n", closing ? ">" : "<" + brk = 0 + } else if (name in para) { + brk = 2 + } else if (name in line) { + if (!brk) brk = 1 + } else if (name == "td" || name == "th") { + text(" ") + } + text(rest) + } + print "" + }' | LC_ALL=C awk ' + { + gsub(/\302\240/, " ") + gsub(/\302[\200-\237]/, "") + gsub(/[ \t\r]+/, " ") + sub(/^ /, "") + sub(/ $/, "") + } + /^\001T/ { + if (title == "") { + title = substr($0, 3) + sub(/^ /, "", title) + } + next + } + /^\001</ { if (!opened) { opened = 1; from = n + 1 }; next } + /^\001>/ { if (opened && !closed) { closed = 1; to = n }; next } + { line[++n] = $0 } + # The title, then the main part of the page when the page marks one, so + # that menus do not fill the preview. At most one blank line in a row. + END { + if (!opened) from = 1 + if (!closed) to = n + if (title != "") { + print title + seen = blank = 1 + } + for (i = from; i <= to; i++) { + if (line[i] == "") { + blank = seen + continue + } + if (blank) print "" + print line[i] + seen = 1 + blank = 0 + } + }' | tr -d '\000-\010\013-\037\177' diff --git a/config.mk b/config.mk old mode 100644 new mode 100755 index 063267d..cf0eeab --- a/config.mk +++ b/config.mk @@ -10,7 +10,8 @@ MANPREFIX=${PREFIX}/share/man # bm-check report dead links (curl) # bm-html the bookmarks as a searchable web page # bm-title lets bm --add suggest the page title as description (curl) +# bm-page shows the text of the page in the fzf preview of bm (curl) # bm-commit lets bm keep the file's history in git # bm-watch brings new browser bookmarks into bm (bm-import, jq) # bm-sync keeps the file the same on all your devices (curl) -TOOLS = bm bm-migrate bm-import bm-check bm-html bm-title bm-commit bm-watch bm-sync +TOOLS = bm bm-migrate bm-import bm-check bm-html bm-title bm-page bm-commit bm-watch bm-sync diff --git a/contrib/windows/sbm.nsi b/contrib/windows/sbm.nsi old mode 100644 new mode 100755 index 3546a0c..81614df --- a/contrib/windows/sbm.nsi +++ b/contrib/windows/sbm.nsi @@ -83,6 +83,7 @@ Section File "${SRC}\bin\bm-html" File "${SRC}\bin\bm-import" File "${SRC}\bin\bm-migrate" + File "${SRC}\bin\bm-page" File "${SRC}\bin\bm-sync" File "${SRC}\bin\bm-title" File "${SRC}\bin\bm-watch" @@ -100,7 +101,7 @@ Section ; Cygwin does not see an execute permission on files that a Windows ; program makes. Cygwin chmod sets it. - nsExec::ExecToLog '"$Cygwin\bin\chmod.exe" 755 /usr/local/bin/bm /usr/local/bin/bm-check /usr/local/bin/bm-commit /usr/local/bin/bm-html /usr/local/bin/bm-import /usr/local/bin/bm-migrate /usr/local/bin/bm-sync /usr/local/bin/bm-title /usr/local/bin/bm-watch /usr/local/bin/fzf /usr/local/libexec/sbm/fzf.exe' + nsExec::ExecToLog '"$Cygwin\bin\chmod.exe" 755 /usr/local/bin/bm /usr/local/bin/bm-check /usr/local/bin/bm-commit /usr/local/bin/bm-html /usr/local/bin/bm-import /usr/local/bin/bm-migrate /usr/local/bin/bm-page /usr/local/bin/bm-sync /usr/local/bin/bm-title /usr/local/bin/bm-watch /usr/local/bin/fzf /usr/local/libexec/sbm/fzf.exe' Pop $0 ${If} $0 != 0 Abort "chmod failed with exit status $0" @@ -163,6 +164,7 @@ Section Uninstall Delete "$Cygwin\usr\local\bin\bm-html" Delete "$Cygwin\usr\local\bin\bm-import" Delete "$Cygwin\usr\local\bin\bm-migrate" + Delete "$Cygwin\usr\local\bin\bm-page" Delete "$Cygwin\usr\local\bin\bm-sync" Delete "$Cygwin\usr\local\bin\bm-title" Delete "$Cygwin\usr\local\bin\bm-watch" diff --git a/packaging/README.md b/packaging/README.md old mode 100644 new mode 100755 index 13385a6..653f57c --- a/packaging/README.md +++ b/packaging/README.md @@ -24,7 +24,7 @@ mentions a sync client. `make install` copies the programs in `TOOLS` to `${DESTDIR}${PREFIX}/bin`: - bm bm-migrate bm-import bm-check bm-html bm-title bm-commit bm-watch + bm bm-migrate bm-import bm-check bm-html bm-title bm-page bm-commit bm-watch Nothing is compiled. There are no man pages. Every package calls `make DESTDIR=... PREFIX=... install` rather than copying the scripts itself, @@ -43,7 +43,7 @@ format is in use, and the user copies them to `~/.local/share/sbm/`. | Required | dmenu **or** fzf | the menu | | Required | xclip **or** xsel **or** wl-clipboard | `bm --copy` | | Recommended | xdg-utils | opening URLs (`xdg-open`) | -| Optional | curl | `bm-title`, `bm-check` | +| Optional | curl | `bm-title`, `bm-page`, `bm-check` | | Optional | jq | `bm-import` of Chromium JSON files | | Optional | git | `bm-commit` | diff --git a/packaging/chocolatey/tools/chocolateyInstall.ps1 b/packaging/chocolatey/tools/chocolateyInstall.ps1 old mode 100644 new mode 100755 index e6a62ca..14996fa --- a/packaging/chocolatey/tools/chocolateyInstall.ps1 +++ b/packaging/chocolatey/tools/chocolateyInstall.ps1 @@ -6,7 +6,7 @@ $toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition $stage = Join-Path $toolsDir 'staging' # What Makefile's TOOLS installs. -$scripts = @('bm', 'bm-migrate', 'bm-import', 'bm-check', 'bm-html', 'bm-title', 'bm-commit', 'bm-watch', 'bm-sync') +$scripts = @('bm', 'bm-migrate', 'bm-import', 'bm-check', 'bm-html', 'bm-title', 'bm-page', 'bm-commit', 'bm-watch', 'bm-sync') # --------------------------------------------------------------------------- # Fetch and unpack. diff --git a/packaging/chocolatey/tools/chocolateyUninstall.ps1 b/packaging/chocolatey/tools/chocolateyUninstall.ps1 old mode 100644 new mode 100755 index a5f567e..f055096 --- a/packaging/chocolatey/tools/chocolateyUninstall.ps1 +++ b/packaging/chocolatey/tools/chocolateyUninstall.ps1 @@ -2,7 +2,7 @@ $ErrorActionPreference = 'Stop' $toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$scripts = @('bm', 'bm-migrate', 'bm-import', 'bm-check', 'bm-html', 'bm-title', 'bm-commit', 'bm-watch', 'bm-sync') +$scripts = @('bm', 'bm-migrate', 'bm-import', 'bm-check', 'bm-html', 'bm-title', 'bm-page', 'bm-commit', 'bm-watch', 'bm-sync') foreach ($s in $scripts) { $cmdPath = Join-Path $toolsDir "$s.cmd" diff --git a/packaging/freebsd/deskutils/sbm/pkg-plist b/packaging/freebsd/deskutils/sbm/pkg-plist old mode 100644 new mode 100755 index 5447749..ccd8d22 --- a/packaging/freebsd/deskutils/sbm/pkg-plist +++ b/packaging/freebsd/deskutils/sbm/pkg-plist @@ -5,6 +5,7 @@ bin/bm-html bin/bm-import bin/bm-migrate bin/bm-title +bin/bm-page bin/bm-watch %%PORTDOCS%%%%DOCSDIR%%/README %%PORTDOCS%%%%DOCSDIR%%/TODO diff --git a/packaging/rpm/sbm.spec b/packaging/rpm/sbm.spec old mode 100644 new mode 100755 index 39359b6..2bd2059 --- a/packaging/rpm/sbm.spec +++ b/packaging/rpm/sbm.spec @@ -81,6 +81,7 @@ make check %{_bindir}/bm-import %{_bindir}/bm-migrate %{_bindir}/bm-title +%{_bindir}/bm-page %{_bindir}/bm-watch %changelog diff --git a/packaging/scoop/sbm.json b/packaging/scoop/sbm.json old mode 100644 new mode 100755 index a846f65..0c1f364 --- a/packaging/scoop/sbm.json +++ b/packaging/scoop/sbm.json @@ -37,7 +37,7 @@ " 'if not defined SBM_SH echo sbm: no POSIX sh found. Install Git: scoop install git 1>&2',", " 'if not defined SBM_SH exit /b 127'", ")", - "foreach ($t in 'bm','bm-migrate','bm-import','bm-check','bm-html','bm-title','bm-commit','bm-watch') {", + "foreach ($t in 'bm','bm-migrate','bm-import','bm-check','bm-html','bm-title','bm-page','bm-commit','bm-watch') {", " ($lines + ('\"%SBM_SH%\" \"%~dp0' + $t + '\" %*')) | Set-Content -LiteralPath \"$dir\\$t.cmd\" -Encoding ASCII", "}" ], @@ -48,6 +48,7 @@ "bm-check.cmd", "bm-html.cmd", "bm-title.cmd", + "bm-page.cmd", "bm-commit.cmd", "bm-watch.cmd" ], diff --git a/test/run.sh b/test/run.sh index 083a1f1..18cb118 100755 --- a/test/run.sh +++ b/test/run.sh @@ -16,6 +16,7 @@ IMPORT="${SBM_SH:-sh} $top/bm-import" CHECK="${SBM_SH:-sh} $top/bm-check" HTML="${SBM_SH:-sh} $top/bm-html" TITLE="${SBM_SH:-sh} $top/bm-title" +PAGE="${SBM_SH:-sh} $top/bm-page" TAB=$(printf '\t') work=${TMPDIR:-/tmp}/sbmtest.$$ @@ -311,7 +312,7 @@ for gone in '--import x' --check --migrate '-s x'; do done bad= -for script in bm bm-migrate bm-import bm-check bm-html bm-title bm-commit bm-watch; do +for script in bm bm-migrate bm-import bm-check bm-html bm-title bm-page bm-commit bm-watch; do [ "$(sed -n 1p "$top/$script")" = '#!/bin/sh' ] || bad="$bad $script" done eq 'every script asks for /bin/sh' "$bad" '' @@ -463,6 +464,78 @@ eq 'a missing tag file is created from the first new tag' \ "$(cat "$USERTAGS")|$(cut -f3 "$BOOKMARKS")" 'first | |first' cp "$here/../usertags" "$USERTAGS" +# ---- bm-page, and the page in the fzf preview ---- + +mkdir "$work/page" +cat > "$work/page/curl" <<'FAKE' +#!/bin/sh +printf 'curl %s\n' "$*" >> "$SBM_TEST_CURLLOG" +for arg in "$@"; do url=$arg; done +case $url in + *rich*) + printf '<!doctype html><html><head><TITLE lang=en>\n A & B — ☺ </Title>\n' + printf '<style>p { color: red } a < b</style><script>if (a < b) document.write("<p>no</p>")</script></head>\n' + printf '<body><!-- a < comment -->\n<h1>Head</h1><p>One\n two <a href="x">link</a> three.</p>\n' + printf '<ul><li>first</li><li>second</li></ul><svg><title>no</title><text>no</text></svg>\n' + printf '<noscript>no</noscript><p>5 < 6 &bogus; A</p></body></html>\n' + ;; + *ctrl*) + printf '<p>esc:\033[31mred\033[0m bel:\007 c1:\302\233x end</p>\n' + ;; + *main*) + printf '<title>T</title><nav>Menu one<br>Menu two</nav><main><h1>Article</h1><p>Body</p></main><footer>Foot</footer>\n' + ;; + *pdf*) + printf '%%PDF-1.7\n1 0 obj\n' + ;; + *big*) + # More than 512k before the text. + awk 'BEGIN { for (i = 0; i < 540000; i++) printf "x"; print "<p>too far</p>" }' + ;; +esac +FAKE +chmod +x "$work/page/curl" + +# page <url>: bm-page with the stand-in curl. +page () { + PATH="$work/page:$PATH" SBM_FETCH=1 $PAGE "$@" +} +nl=' +' + +eq 'bm-page prints the title, then the text without scripts, styles, comments and svg' \ + "$(page https://rich.example/)" \ + "A & B — ☺${nl}${nl}Head${nl}${nl}One two link three.${nl}${nl}first${nl}second${nl}${nl}5 < 6 &bogus; A" +out=$(page https://ctrl.example/) +eq 'bm-page removes control characters, so that a page cannot drive the terminal' \ + "$out|$(printf '%s' "$out" | LC_ALL=C tr -d '\040-\176' | wc -c | tr -d ' ')" 'esc:[31mred[0m bel: c1:x end|0' +eq 'bm-page shows only the main part of a page that marks one' \ + "$(page https://main.example/)" "T${nl}${nl}Article${nl}${nl}Body" +eq 'bm-page names a PDF document' "$(page https://pdf.example/)" '(a PDF document)' +eq 'bm-page reads at most 512k of a page' "$(page https://big.example/ | grep -c 'too far')" '0' +: > "$SBM_TEST_CURLLOG" +eq 'bm-page with SBM_FETCH=0 prints nothing' \ + "$(PATH="$work/page:$PATH" SBM_FETCH=0 $PAGE https://rich.example/)" '' +eq 'bm-page fetches only http and https pages' \ + "$(page ftp://rich.example/)$(page 'javascript:alert(1)')$(page rich.example)" '' +eq 'bm-page with SBM_FETCH=0, or for another scheme, never runs curl' "$(cat "$SBM_TEST_CURLLOG")" '' +eq 'bm-page takes an HTTPS address in capitals' "$(page HTTPS://main.example/ | sed -n 1p)" 'T' +$PAGE 2>/dev/null +eq 'bm-page wants exactly one url' "$?" '2' +$PAGE https://a.example https://b.example 2>/dev/null +eq 'bm-page wants exactly one url, not two' "$?" '2' + +# The preview command of the fzf list, as fzf runs it: each placeholder +# becomes its field in single quotes. +preview=$(sed -n 's/^PREVIEW="\(.*\)"$/\1/p' "$top/bm" | + sed "s|{1}|'https://main.example/'|g; s|{2}|'Main page'|; s|{3}|'code'|; s|{4}|'3'|") +eq 'the fzf preview shows the bookmark, then the text of its page' \ + "$(PATH="$top:$work/page:$PATH" SBM_FETCH=1 ${SBM_SH:-sh} -c "$preview")" \ + "https://main.example/${nl}Main page${nl}tags: code${nl}used: 3${nl}${nl}T${nl}${nl}Article${nl}${nl}Body" +eq 'the fzf preview shows the bookmark without bm-page' \ + "$(PATH="$work/page:/usr/bin:/bin" SBM_FETCH=1 ${SBM_SH:-sh} -c "$preview" 2>&1)" \ + "https://main.example/${nl}Main page${nl}tags: code${nl}used: 3" + # ---- bm-import and bm --merge ---- reset @@ -726,13 +799,13 @@ eq 'browse: text that matches nothing is searched for on the web' \ answers 'ctrl-s:' '' fz </dev/null eq 'browse: the preview follows the cursor' \ - "$(grep -c -e "--preview=printf '%s.n%s.ntags: %s.nused: %s.n' {1} {2} {3} {4}" "$SBM_TEST_LOG")" '2' + "$(grep -c -e "--preview=printf '%s.n%s.ntags: %s.nused: %s.n.n' {1} {2} {3} {4}; sleep 0.3 2>/dev/null; bm-page {1} 2>/dev/null" "$SBM_TEST_LOG")" '2' : > "$SBM_TEST_LOG" answers 'Mid' fz -c </dev/null eq 'fzf: rows are listed by their display column, with a live preview' \ - "$(grep -c -e "--with-nth=5.. --preview-window=down:5:wrap --preview=printf '%s" "$SBM_TEST_LOG")" '1' + "$(grep -c -e "--with-nth=5.. --preview-window=right:50%:wrap --preview=printf '%s" "$SBM_TEST_LOG")" '1' : > "$SBM_TEST_LOG" answers 'Mid' SBM_FZF_OPTS='--height=100%' fz -c </dev/null