Commit 651bc655 authored by  's avatar  

Syncing from Monotone at 20190128-120909. Last commit message was Try fixing radiobox handling

parents
.*[.](bin|upstream-doc)$
(in-package :thoughtful-theridion)
(defvar *default-user-agent*
"Thoughtful-Theridion/0.0 (magic matching strings: Gecko Firefox)")
(defclass http-fetcher ()
((cookie-jar :accessor cookie-jar
:initform (make-instance 'drakma:cookie-jar))
(pending-redirect :accessor pending-redirect :initform nil)
(current-url :accessor current-url :initform nil)
(current-intended-url :accessor current-intended-url :initform nil)
(current-status-code :accessor current-status-code :initform nil)
(current-status-line :accessor current-status-line :initform nil)
(current-headers :accessor current-headers :initform nil)
(current-content :accessor current-content :initform nil)
(current-content-bytes :accessor current-content-bytes :initform nil)
(parsed-content :accessor parsed-content :initform nil)
(parser-warnings :accessor parser-warnings :initform nil)
(referer-policy :accessor referer-policy
:initform (lambda (&key old new)
(declare (ignorable old))
new)
:initarg :referer-policy)
(redirect-policy :accessor redirect-policy
:initform
(lambda (&key code header old new)
(declare (ignorable code header old new))
nil)
:initarg :redirect-policy)
(basic-auth-policy :accessor basic-auth-policy
:initform (lambda (&key url)
(declare (ignorable url)) nil)
:initarg :basic-auth-policy)
(user-agent-policy :accessor user-agent
:initform (lambda (&key url) (declare (ignorable url))
*default-user-agent*)
:initarg :user-agent)
(decode-bytes-policy :accessor decode-bytes-policy
:initform (lambda (&key content content-type
url headers)
(declare (ignorable url headers))
(let* ((decompressed (ignore-errors
(chipz:decompress
nil 'chipz:gzip content))))
(decode-guessed-encoding
:content (or decompressed content)
:content-type content-type)))
:initarg :decode-bytes-policy)
(parsing-policy
:accessor parsing-policy
:initform (lambda (&key content-type content url
headers code)
(declare (ignorable url headers code))
(cond ((cl-ppcre:scan "^text/html(;|$)" content-type)
(ignore-errors (html5-parser:parse-html5 content)))
((cl-ppcre:scan "^(text|application)/(x-)?json(;|$)"
content-type)
(ignore-errors (cl-json:decode-json-from-string
content)))
(t (let* ((document (html5-parser:make-document))
(pre (html5-parser:make-element document "pre" nil))
(text (html5-parser:make-text-node document content)))
(html5-parser:node-append-child pre text)
(html5-parser:node-append-child document pre)
document))))
:initarg :parsing-policy)
(proxy :accessor proxy :initform drakma:*default-http-proxy*
:initarg :proxy)
))
(defgeneric parse-obtained-content (fetcher))
(defmethod parse-obtained-content ((fetcher http-fetcher))
(setf (current-content fetcher)
(funcall (decode-bytes-policy fetcher)
:allow-other-keys t
:url (current-url fetcher)
:content-type (cdr (assoc :content-type (current-headers fetcher)))
:headers (current-headers fetcher)
:content (current-content-bytes fetcher)))
(if (current-content fetcher)
(multiple-value-bind (parsed warnings)
(funcall (parsing-policy fetcher)
:allow-other-keys t
:content (current-content fetcher)
:url (current-url fetcher) :code (current-status-code fetcher)
:content-type (cdr (assoc :content-type (current-headers fetcher)))
:headers (current-headers fetcher))
(setf (parsed-content fetcher) parsed
(parser-warnings fetcher) warnings))
(setf (parsed-content fetcher) nil
(parser-warnings fetcher) "Content is binary")))
(defgeneric navigate (fetcher url &rest drakma-args))
(defmethod navigate ((fetcher http-fetcher) (url string) &rest drakma-args)
(when (and (not (cl-ppcre:scan "^[a-z]+:" (string-downcase url)))
(cl-ppcre:scan "^[a-z]+[%]3a" (string-downcase url)))
(setf url (urldecode url :latin-1)))
(setf url (cl-ppcre:regex-replace "#.*" url ""))
(setf url (urlencode-unsafe url :utf-8))
(multiple-value-bind
(content
status-code server-headers
reply-url
reply-stream reply-stream-needs-closing-p
status-line)
(handler-case
(apply 'drakma:http-request url
(append
(let* ((additional-headers
(getf drakma-args :additional-headers)))
(list
:additional-headers
(append
additional-headers
(unless (assoc "Referer" additional-headers :test 'equalp)
`(("Referer" . ,(funcall (referer-policy fetcher)
:old (current-url fetcher)
:new url
:allow-other-keys t)))))))
drakma-args
(list
:cookie-jar (cookie-jar fetcher)
:user-agent (funcall (user-agent fetcher)
:allow-other-keys t
:url url)
:proxy (proxy fetcher)
:redirect nil
:force-binary t
)))
(error (e)
(values
(babel:string-to-octets (format nil "~a" e) :encoding :utf-8)
503
`((:content-type . "text/plain; charset=utf-8"))
nil nil nil
"Fetching failed")))
(when reply-stream-needs-closing-p (close reply-stream))
(when (getf drakma-args :cookie-jar)
(setf (cookie-jar fetcher) (getf drakma-args :cookie-jar)))
(setf
(current-content-bytes fetcher) content
(current-status-code fetcher) status-code
(current-status-line fetcher) status-line
(current-url fetcher) reply-url
(current-intended-url fetcher) url
(current-headers fetcher) server-headers
)
(parse-obtained-content fetcher)
(or
(and (= status-code 401)
(null (getf drakma-args :basic-authorization))
(let* ((auth-data (funcall (basic-auth-policy fetcher)
:url url
:allow-other-keys t)))
(when auth-data
(apply 'navigate fetcher url
:basic-authorization auth-data
drakma-args))))
(and
(or (<= 300 status-code 399)
(assoc :location server-headers)
(assoc :refresh server-headers))
(let* ((location (cdr (assoc :location server-headers)))
(refresh (cl-ppcre:regex-replace
"^ *[0-9]+ *; *"
(cdr (assoc :refresh server-headers)) ""))
(policy-url
(funcall (redirect-policy fetcher)
:status-code status-code
:header (cond (location :location)
(refresh :refresh)
(t nil))
:old url
:new (or location refresh reply-url url)
:content content
:allow-other-keys t)))
(when policy-url (apply 'navigate fetcher policy-url drakma-args))))
(current-content fetcher))))
#! /bin/sh
cd "$(dirname "$0")"
NO_RLWRAP=1 ./nix-load-lisp.sh --eval '
(progn
(setf cffi:*foreign-library-directories*
(cffi::explode-path-environment-variable
"NIX_LISP_LD_LIBRARY_PATH"))
(loop
with libpath :=
(uiop:split-string
(uiop:getenv "NIX_LISP_LD_LIBRARY_PATH")
:separator ":")
for l in sb-alien::*shared-objects*
for ns := (sb-alien::shared-object-namestring l)
do (and (> (length ns) 0) (not (equal (elt ns 0) "/"))
(let*
((prefix (find-if
(lambda (s)
(probe-file (format nil "~a/~a" s ns)))
libpath))
(fullpath (and prefix (format nil "~a/~a" prefix ns))))
(when fullpath
(setf
(sb-alien::shared-object-namestring l) fullpath
(sb-alien::shared-object-pathname l)
(probe-file fullpath))))))
(sb-ext:save-lisp-and-die "./thoughtful-theridion.bin" :executable t))
'
(in-package :thoughtful-theridion)
(defparameter *babel-encodings* (make-hash-table :test 'equalp))
(maphash (lambda (k v)
(setf (gethash (string-downcase k) *babel-encodings*) v))
babel-encodings::*character-encodings*)
(maphash (lambda (k v)
(loop
with ks := (string-downcase k)
for k-prime in
(list
(cl-ppcre:regex-replace-all "-" ks "")
(cl-ppcre:regex-replace-all "[^a-z0-9]" ks "")
(cl-ppcre:regex-replace-all "[^a-z0-9]" ks "-")
(cl-ppcre:regex-replace-all
"([a-z])([0-9])|([0-9])([a-z])" ks "\\1\\3-\\2\\4"))
do
(setf (gethash k-prime *babel-encodings*)
(or (gethash k-prime *babel-encodings*) v))))
babel-encodings::*character-encodings*)
(defun content-type-encoding (content-type)
(let* ((components (cl-ppcre:split " *(;|$) *" content-type))
(options (rest components))
(options-split (mapcar (lambda (x) (cl-ppcre:split "=" x)) options))
(option-charset (find "charset" options-split
:key 'first :test 'equalp))
(charset (second option-charset))
(charset (cl-ppcre:regex-replace-all "\"" charset "")))
charset))
(defun html-meta-encoding (data)
(cond
((typep data '(array (unsigned-byte 8)))
(html-meta-encoding (babel:octets-to-string data :encoding :latin-1)))
((typep data 'string)
(html-meta-encoding (html5-parser:parse-html5 data)))
(t
(ignore-errors
(content-type-encoding
(html5-parser:element-attribute
(css-selectors:query1
"meta[http-equiv='Content-Type']" data)
"content"))))))
(defun extract-encoding (data content-type)
(or
(content-type-encoding content-type)
(html-meta-encoding data)
"utf-8"))
(defun decode-guessed-encoding (&key content content-type)
(when content
(babel:octets-to-string
content
:encoding
(or
(gethash (extract-encoding content content-type) *babel-encodings*)
:utf-8)
:errorp nil)))
#! /bin/sh
cd "$(dirname "$0")"
./thoughtful-theridion.bin --non-interactive --eval "
(let*
((buffer (make-array ${2:-(expt 10 7)} :element-type (quote (unsigned-byte 8))))
(fetcher (make-instance 'thoughtful-theridion:http-fetcher))
(n (read-sequence buffer *standard-input*))
(buffer (subseq buffer 0 n)))
(setf (thoughtful-theridion:current-content-bytes fetcher) buffer
(thoughtful-theridion:current-headers fetcher) (quote ((:content-type . \"text/html\")))
(thoughtful-theridion:current-url fetcher) \"$1\"
(thoughtful-theridion:current-status-code fetcher) 200)
(thoughtful-theridion:parse-obtained-content fetcher)
(write
(or (ignore-errors
(thoughtful-theridion:html-element-to-text
(make-instance (quote thoughtful-theridion:html-textifier-protocol-inspector))
(thoughtful-theridion:parsed-content fetcher)))
\"\")))"
(in-package :thoughtful-theridion)
(defun select-form (data &optional (selector "form"))
(cond
((stringp data)
(select-form (html5-parser:parse-html5 data)))
((and (equal (html5-parser:node-type data) :element)
(equal (html5-parser:node-name data) "form"))
data)
(t (loop for e := (css-selectors:query1 selector data)
then (html5-parser:node-parent e)
while e
when (equal (html5-parser:node-type e) :element)
when (equal (html5-parser:node-name e) "form")
return e))))
(defun input-entry (data assignments)
(cond
((stringp data)
(input-entry
(html5-parser:node-last-child (html5-parser:parse-html5-fragment data))
assignments))
(t (let* ((name (html5-parser:element-attribute data "name"))
(tag (html5-parser:node-name data))
(type (html5-parser:element-attribute data "type"))
(value (html5-parser:element-attribute data "value"))
(checked (html5-parser:element-attribute data "checked"))
(checkedp (> (length checked) 0))
(assigned (assoc name assignments :test 'equal)))
(cond (assigned nil)
((null name) nil)
((equal type "checkbox")
(when checkedp (cons name "1")))
((equal type "radio")
(when checkedp (cons name value)))
((equal tag "select")
(let* ((selected-option
(first
(remove-if
(lambda (e)
(= 0 (length
(html5-parser:element-attribute
e "selected"))))
(css-selectors:query "option[selected]" data))))
(selected-value
(and selected-option
(html5-parser:element-attribute
selected-option "value"))))
(when selected-option (list name selected-value))))
(t (when value (cons name value))))))))
(defun form-entries (data assignments)
(cond
((stringp data)
(form-entries
(html5-parser:node-last-child (html5-parser:parse-html5-fragment data))
assignments))
(t (let* ((inputs (css-selectors:query "input" data))
(selects (css-selectors:query "select" data))
(textareas (css-selectors:query "textarea" data))
(buttons (css-selectors:query "button" data))
(elements (append inputs selects textareas buttons))
(names (loop for e in elements collect
(html5-parser:element-attribute e "name")))
(name-ht (make-hash-table :test 'equal))
(names-list nil)
(base-entries (append assignments
(loop for e in elements collect
(input-entry e assignments)))))
(loop for n in names when n do (incf (gethash n name-ht 0)))
(maphash (lambda (k v) v (push k names-list)) name-ht)
(setf names-list (sort names-list 'string<))
(values
(loop for e in base-entries
for n := (car e)
for k := (gethash n name-ht 0)
collect e
when (> k 1) collect
(cons (format nil "~a[]" (car e)) (cdr e)))
names-list)))))
(defun form-parameters (form &key
assignments (url *base-url*) drakma-args
fetcher (form-selector "form"))
(let* ((form (or form
(and fetcher
(css-selectors:query1 form-selector
(parsed-content fetcher)))))
(url (or url (and fetcher
(current-url fetcher)
(format nil "~a" (current-url fetcher)))))
(form-entries-values
(multiple-value-list
(form-entries form assignments)))
(form-entry-names (second form-entries-values))
(form-entries (first form-entries-values))
(method (or (html5-parser:element-attribute form "method")
"get"))
(method (intern (string-upcase method) :keyword))
(action (or (html5-parser:element-attribute form "action")
url))
(action (if (equal action "") url action))
(action (let ((*base-url* url)) (real-url action)))
(encoding (or (html5-parser:element-attribute form "enctype")
"application/x-www-form-urlencoded")))
(append
(list action)
(list :method method :content-type encoding :parameters form-entries)
(list
:known-parameters (loop for x in form-entry-names collect `(,x . ""))
:allow-other-keys t)
drakma-args
(list :external-format-out :utf-8))))
(defun submit-form (fetcher assignments &key
(form-selector "form")
chosen-form
(drakma-args nil))
(let* ((document (parsed-content fetcher))
(form (or chosen-form (select-form document form-selector)))
(url (format nil "~a" (current-url fetcher)))
(parameters (form-parameters
form :assignments assignments
:url (or url *base-url*)
:drakma-args drakma-args)))
(apply 'navigate fetcher parameters)))
(in-package :thoughtful-theridion)
(defvar *base-url* nil)
(defun real-url (url)
(cond ((null *base-url*) url)
((equal url "") *base-url*)
((cl-ppcre:scan "^[a-z]+:" url) url)
((cl-ppcre:scan "^//" url)
(format nil "~a~a"
(cl-ppcre:regex-replace
"(^[a-z]+:)/.*$" *base-url* "\\1")
url))
((cl-ppcre:scan "^/" url)
(format nil "~a~a"
(cl-ppcre:regex-replace
"(^[a-z]+:/*[^/]+)/.*$" *base-url* "\\1")
url))
(t (format nil "~a/~a"
(cl-ppcre:regex-replace
"(^[a-z]+:/*[^/].*)/.*$" *base-url* "\\1")
url))))
(defclass html-textifier-protocol () ())
(defgeneric html-element-to-text (protocol element &key base-url))
(defgeneric html-element-to-text-dispatch (protocol type tag element))
(defmethod html-element-to-text
((protocol html-textifier-protocol) element &key base-url)
(let* ((*base-url* (or base-url *base-url*)))
(html-element-to-text-dispatch
protocol (html5-parser:node-type element)
(and (html5-parser:node-name element)
(intern (string-upcase (html5-parser:node-name element)) :keyword))
element)))
(defmethod html-element-to-text
((protocol html-textifier-protocol) (element null) &key base-url)
(declare (ignorable base-url))
"")
(defmethod html-element-to-text
((protocol html-textifier-protocol) (element http-fetcher) &key base-url)
(html-element-to-text
protocol (parsed-content element)
:base-url (or base-url
(and (current-url element)
(format nil "~a" (current-url element)))
*base-url*)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :comment)) tag element)
"")
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :document-type)) tag element)
"")
(defparameter *whitespace-list* `(#\Newline #\Return #\Space #\Tab))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :text)) tag element)
(cl-ppcre:regex-replace-all
(if *whitespace-list* (format nil "[~{~a~}]+" *whitespace-list*) " ")
(html5-parser:node-value element)
" "))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :element)) tag element)
(apply 'concatenate 'string
(loop for c := (html5-parser:node-first-child element)
then (html5-parser:node-next-sibling c)
while c collect
(html-element-to-text protocol c))))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :element)) (tag (eql :pre))
element)
(let* ((*whitespace-list* nil))
(call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol) (type (eql :document)) tag element)
(apply 'concatenate 'string
(loop for c := (html5-parser:node-first-child element)
then (html5-parser:node-next-sibling c)
while c collect
(html-element-to-text protocol c))))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :document-fragment)) tag element)
(apply 'concatenate 'string
(loop for c := (html5-parser:node-first-child element)
then (html5-parser:node-next-sibling c)
while c collect
(html-element-to-text protocol c))))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :br)) element)
(format nil "~%"))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :script)) element)
"")
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :style)) element)
"")
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :div)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :p)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :li)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :ol)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :ul)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :tr)) element)
(format nil "~%~a~%" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol html-textifier-protocol)
(type (eql :element)) (tag (eql :table)) element)
(format nil "~%~a~%" (call-next-method)))
(defclass show-urls-mixin (html-textifier-protocol) ())
(defmethod html-element-to-text-dispatch
((protocol show-urls-mixin) (type (eql :element)) tag element)
(let* ((attributes '("href" "src" "data-expanded-url" "data-url"))
(urls (loop for a in attributes
for v := (html5-parser:element-attribute element a)
when v collect (real-url v)))
(urls (loop for u in urls
for du := (ignore-errors (urldecode u :utf-8))
collect u
when (and du (not (equal u du)))
collect du))
(text (call-next-method)))
(if urls (format nil "{ ~a }~{→[ ~a ]~}" text urls) text)))
(defmethod html-element-to-text-dispatch
((protocol show-urls-mixin) (type (eql :element)) (tag (eql :meta)) element)
(let ((attributes nil))
(html5-parser:element-map-attributes
(lambda (k ns v) (push (list k v ns) attributes))
element)
(format nil "[meta]:{ ~{~{~a: { ~a }~} ~}}"
(loop for x in attributes
for k := (first x)
for v := (second x)
for ns := (third x)
for ve := ""
when (and
(equalp k "content")
(equalp
(html5-parser:element-attribute
element "http-equiv") "refresh")
(cl-ppcre:scan "^ *[0-9]+ *; *" v))
do (setf
ve (format
nil "~a →[ ~a ]" ve
(real-url
(cl-ppcre:regex-replace
"^ *[0-9]+ *; *((URL|url)=)?" v ""))))
collect (list
(format nil "~a:~a" (or ns "") k)
(concatenate 'string v ve))))))
(defclass show-ui-texts-mixin (html-textifier-protocol) ())
(defmethod html-element-to-text-dispatch
((protocol show-ui-texts-mixin) (type (eql :element)) tag element)
(let* ((attributes '("name" "id" "alt" "title"))
(names (loop for a in attributes
for v := (html5-parser:element-attribute element a)
when v collect v))
(text (call-next-method)))
(if names (format nil "~{ ⚓[ ~a ]~}~a" names text) text)))
(defclass show-formatting-mixin (html-textifier-protocol) ())
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :s)) element)
(format nil "{-- ~a --}" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :i)) element)
(format nil "{// ~a //}" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :b)) element)
(format nil "{** ~a **}" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :u)) element)
(format nil "{__ ~a __}" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :sup)) element)
(format nil "{↑↑ ~a ↑↑}" (call-next-method)))
(defmethod html-element-to-text-dispatch
((protocol show-formatting-mixin)
(type (eql :element)) (tag (eql :sub)) element)
(format nil "{↓↓ ~a ↓↓}" (call-next-method)))
(defclass recurse-on-noscript-mixin (html-textifier-protocol) ())
(defmethod html-element-to-text-dispatch
((protocol recurse-on-noscript-mixin)
(type (eql :element)) (tag (eql :noscript))
element)
(html-element-to-text
protocol
(when (html5-parser:node-first-child element)
(html5-parser:parse-html5-fragment
(html5-parser:node-value
(html5-parser:node-first-child
element))))))
(defclass html-textifier-protocol-inspector
(recurse-on-noscript-mixin
show-formatting-mixin show-ui-texts-mixin show-urls-mixin
html-textifier-protocol) ())
#! /bin/sh
unset NIX_LISP NIX_LISP_ASDF NIX_LISP_ASDF_PATHS NIX_LISP_COMMAND NIX_LISP_EARLY_OPTIONS NIX_LISP_EXEC_CODE NIX_LISP_LD_LIBRARY_PATH NIX_LISP_LOAD_FILE ASDF_OUTPUT_TRANSLATIONS
unset $(set | grep '^_.*NIX_LISP*' | cut -d = -f 1)
NIX_LISP_ASDF_PATHS="$(realpath "$(dirname "$0")")
$PWD"
for i in drakma cl_plus_ssl cl-html5-parser parenscript cl-ppcre cl-json css-selectors css-selectors-simple-tree babel $THOUGHTFUL_THERIDION_EXTRA_DEPENDENCIES; do
source "$(nix-build --no-out-link '<nixpkgs>' -A lispPackages.$i)"/lib/common-lisp-settings/*-path-config.sh
done
export ASDF_OUTPUT_TRANSLATIONS=/nix/store:/nix/store
if test -n "$NO_RLWRAP"; then WRAP=sh; else
WRAP="$(nix-build --no-out-link '<nixpkgs>' -A rlwrap)"/bin/rlwrap;
fi
"$WRAP" "$(nix-build --no-out-link '<nixpkgs>' -A lispPackages.clwrapper)"/bin/cl-wrapper.sh "$(nix-build --no-out-link '<nixpkgs>' -A sbcl)"/bin/sbcl $SBCL_EARLY_OPTIONS --eval "(require :thoughtful-theridion)" "$@"
(defpackage :thoughtful-theridion
(:use :common-lisp)
(:export
#:*default-user-agent*
#:http-fetcher
#:navigate
#:submit-form
#:html-textifier-protocol
#:html-textifier-protocol-inspector
#:html-element-to-text
#:save-web-page
#:save-web-form
#:parsed-content
#:current-url
#:current-content
#:current-content-bytes
#:current-headers
#:current-status-code
#:parse-obtained-content
#:*base-url*
#:real-url
))
<