bm-page (7652 bytes)
1 #!/bin/sh 2 3 # bm-page: print a web page as plain text: its title, a blank line, then its 4 # text. When it is installed, the fzf list of bm shows the page of the 5 # bookmark under the cursor in the preview, live. SBM_FETCH=0 keeps it off 6 # the network. 7 # 8 # bm-page https://suckless.org/ | less 9 # 10 # Only http and https pages are fetched. curl stops after 5 seconds, and dd 11 # passes at most 512k of the page. Scripts, styles and comments are left 12 # out. Control characters are removed, so that a page cannot send escape 13 # sequences to the terminal. 14 15 if [ $# -ne 1 ]; then 16 printf 'usage: bm-page <url>\n' >&2 17 exit 2 18 fi 19 [ "${SBM_FETCH:-1}" != 0 ] || exit 0 20 case $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') in 21 http://*|https://*) ;; 22 *) exit 0 ;; 23 esac 24 25 # The first awk reads the page as one record (RS is a control character), 26 # then splits it at each "<". Each part starts with a tag and ends with the 27 # text after it, so one pass through the parts reads the page in linear 28 # time. The second awk tidies the white space. 29 curl -sL --max-time 5 -A 'Mozilla/5.0' -- "$1" 2>/dev/null \ 30 | dd bs=1024 count=512 2>/dev/null | LC_ALL=C awk ' 31 function utf8(c) { 32 if (c == 160) return " " 33 if (c < 128) return sprintf("%c", c) 34 if (c < 2048) return sprintf("%c%c", 192 + int(c / 64), 128 + c % 64) 35 if (c < 65536) 36 return sprintf("%c%c%c", 224 + int(c / 4096), 128 + int(c / 64) % 64, 128 + c % 64) 37 if (c < 1114112) 38 return sprintf("%c%c%c%c", 240 + int(c / 262144), 128 + int(c / 4096) % 64, 39 128 + int(c / 64) % 64, 128 + c % 64) 40 return "" 41 } 42 function entity(name, n, i) { 43 if (name ~ /^#[0-9]+$/) return utf8(substr(name, 2) + 0) 44 if (name ~ /^#[xX][0-9A-Fa-f]+$/) { 45 n = 0 46 for (i = 3; i <= length(name); i++) 47 n = n * 16 + index("0123456789abcdef", tolower(substr(name, i, 1))) - 1 48 return utf8(n) 49 } 50 if (name in named) return named[name] 51 return "&" name ";" 52 } 53 function decode(s, out, i, j, name) { 54 out = "" 55 while ((i = index(s, "&")) > 0) { 56 out = out substr(s, 1, i - 1) 57 s = substr(s, i + 1) 58 j = index(s, ";") 59 name = substr(s, 1, j - 1) 60 if (j > 1 && j <= 12 && name ~ /^(#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*)$/) { 61 out = out entity(name) 62 s = substr(s, j + 1) 63 } else { 64 out = out "&" 65 } 66 } 67 return out s 68 } 69 # text: the text of the page, or of its title. As in a browser, a line 70 # break in the text is a space. The line breaks come from the tags: brk 71 # tells how many are waiting for the next text. 72 function text(s) { 73 if (intitle) { 74 title = title " " s 75 return 76 } 77 s = decode(s) 78 gsub(/[ \t\r\n]+/, " ", s) 79 if (s == "" || s == " ") { 80 if (s == " " && !brk) printf " " 81 return 82 } 83 if (brk) { 84 printf "%s", brk == 2 ? "\n\n" : "\n" 85 brk = 0 86 sub(/^ /, "", s) 87 } 88 printf "%s", s 89 } 90 BEGIN { 91 RS = "\001" 92 named["amp"] = "&"; named["lt"] = "<"; named["gt"] = ">"; named["quot"] = "\"" 93 named["apos"] = "\047"; named["nbsp"] = " " 94 named["ndash"] = utf8(8211); named["mdash"] = utf8(8212); named["hellip"] = utf8(8230) 95 named["lsquo"] = utf8(8216); named["rsquo"] = utf8(8217) 96 named["ldquo"] = utf8(8220); named["rdquo"] = utf8(8221) 97 named["laquo"] = utf8(171); named["raquo"] = utf8(187); named["middot"] = utf8(183) 98 named["bull"] = utf8(8226); named["copy"] = utf8(169); named["reg"] = utf8(174) 99 named["trade"] = utf8(8482); named["deg"] = utf8(176); named["times"] = utf8(215) 100 named["euro"] = utf8(8364) 101 # Tags that start a line, and tags that start a paragraph. 102 split("br div li tr dt dd header footer nav main section article aside " \ 103 "figure figcaption form details summary address hr", b, " ") 104 for (i in b) line[b[i]] = 1 105 split("p h1 h2 h3 h4 h5 h6 ul ol dl table blockquote pre", b, " ") 106 for (i in b) para[b[i]] = 1 107 # Tags whose content is no text of the page. 108 split("script style noscript template svg iframe object", b, " ") 109 for (i in b) hidden[b[i]] = 1 110 } 111 { page = page $0 } 112 END { 113 if (substr(page, 1, 5) == "%PDF-") { 114 print "(a PDF document)" 115 exit 116 } 117 n = split(page, part, "<") 118 text(part[1]) 119 for (i = 2; i <= n; i++) { 120 p = part[i] 121 if (comment || substr(p, 1, 3) == "!--") { 122 k = index(p, "-->") 123 comment = !k 124 if (k && skip == "") text(substr(p, k + 3)) 125 continue 126 } 127 k = index(p, ">") 128 if (!k) { 129 # A "<" in the text, or the end of a page that dd cut off. 130 if (skip == "" && i < n) text("<" p) 131 continue 132 } 133 tag = tolower(substr(p, 1, k - 1)) 134 closing = substr(tag, 1, 1) == "/" 135 name = tag 136 sub(/^\//, "", name) 137 sub(/[^a-z0-9].*/, "", name) 138 rest = substr(p, k + 1) 139 if (skip != "") { 140 if (closing && name == skip) { 141 skip = "" 142 text(rest) 143 } 144 continue 145 } 146 if (name in hidden && !closing && substr(tag, length(tag)) != "/") { 147 skip = name 148 continue 149 } 150 if (name == "title" && !done) { 151 if (closing) { 152 intitle = 0 153 done = 1 154 t = decode(title) 155 gsub(/[ \t\r\n]+/, " ", t) 156 printf "\n\001T%s\n", t 157 brk = 0 158 } else { 159 intitle = 1 160 } 161 } else if (name == "main") { 162 # Marks for the second awk: the main part of the page. 163 printf "\n\001%s\n", closing ? ">" : "<" 164 brk = 0 165 } else if (name in para) { 166 brk = 2 167 } else if (name in line) { 168 if (!brk) brk = 1 169 } else if (name == "td" || name == "th") { 170 text(" ") 171 } 172 text(rest) 173 } 174 print "" 175 }' | LC_ALL=C awk ' 176 { 177 gsub(/\302\240/, " ") 178 gsub(/\302[\200-\237]/, "") 179 gsub(/[ \t\r]+/, " ") 180 sub(/^ /, "") 181 sub(/ $/, "") 182 } 183 /^\001T/ { 184 if (title == "") { 185 title = substr($0, 3) 186 sub(/^ /, "", title) 187 } 188 next 189 } 190 /^\001</ { if (!opened) { opened = 1; from = n + 1 }; next } 191 /^\001>/ { if (opened && !closed) { closed = 1; to = n }; next } 192 { line[++n] = $0 } 193 # The title, then the main part of the page when the page marks one, so 194 # that menus do not fill the preview. At most one blank line in a row. 195 END { 196 if (!opened) from = 1 197 if (!closed) to = n 198 if (title != "") { 199 print title 200 seen = blank = 1 201 } 202 for (i = from; i <= to; i++) { 203 if (line[i] == "") { 204 blank = seen 205 continue 206 } 207 if (blank) print "" 208 print line[i] 209 seen = 1 210 blank = 0 211 } 212 }' | tr -d '\000-\010\013-\037\177'