;; sqlplus-html.el -- Render SQL*Plus HTML output on-the-fly.
;; Copyright (C) 2001 Hrvoje Niksic
;; Author: Hrvoje Niksic
;; Keywords: database, hypermedia, commwww
;; Version: 0.97
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; This package might be useful to people who use Oracle's SQL*Plus in
;; a shell buffer. It massages the output of SQL*Plus to format it
;; into nice-looking tables a la mysql's command line client. This is
;; feasible thanks to the fact that SQL*Plus has an option to produce
;; HTML output, and that links and w3 handle HTML tables nicely.
;; For the mode to work, you will need a working `w3' package or the
;; `links' external browser (`lynx' won't do because it doesn't handle
;; tables.) Start the `sqlplus' client in a comint (i.e. shell)
;; buffer. Then execute `M-x sqlplus-html-init-all', and you should
;; be set.
;; To test whether this works as intended, issue a simple query such
;; as "select 2+2 from dual". The result should look like this:
;; SQL>select 2+2 from dual;
;;
;; +---+
;; |2+2|
;; |---|
;; | 4 |
;; +---+
;; sqlplus-html-init-all does two things: sends "set markup html on"
;; to SQL*Plus to make it write all output in HTML; and and puts the
;; buffer in sqlplus-html minor mode so that the HTML output is
;; rendered on the fly.
;; You can disable the mode at any time with `M-x sqlplus-html-mode',
;; which works as a toggle.
;; The latest version should be available at:
;;
;;
;;
;; Thanks go to:
;; * Drazen Kacar , for introducing me to "set
;; markup html on".
;;; Code:
(require 'cl)
(require 'comint)
(defvar sqlplus-html-mode nil
"A mode for rendering SQL*Plus HTML output.")
(make-variable-buffer-local 'sqlplus-html-mode)
;; This belongs to "cross-Emacs compatibility" section below, but we
;; need it to define sqlplus-html-process-method.
(defun sqlplus-html-find-executable (name)
;; First try the easy and fast way, using locate-file. We cannot
;; check for (fboubdp 'locate-file) because we can't check if it
;; supports the new interface.
(block out
(condition-case nil
(return-from out
(locate-file name (split-path (getenv "PATH")) nil 'executable))
(error nil))
;; The hard way.
(dolist (directory (split-string (getenv "PATH") ":"))
(if (file-executable-p (expand-file-name name directory))
(return-from out t)))
nil))
(defvar sqlplus-html-process-method
(cond ((sqlplus-html-find-executable "links")
;; Links is preferred due to extremely fast startup and
;; operation.
'links)
((sqlplus-html-find-executable "w3m")
;; w3m is also OK.
'w3m)
((locate-library "w3")
;; w3 is unacceptably slow when rendering tables, but it's
;; still better than nothing.
(require 'w3)
'w3)
(t
(error
"Please install `links' or `w3m' browsers, or the `w3' elisp library")))
"*Method for processing HTML.
Currently valid methods are symbols `links', `w3m', and `w3'.")
(defvar sqlplus-html-prompt-regexp "\n\r?SQL> \\'"
"Regexp that matches the HTML version of the SQL*Plus prompt.")
;; Receive progress params.
(defvar sqlplus-html-receive-progress-threshold 0
"Don't print progress messages before this many bytes are received.")
(defvar sqlplus-html-receive-progress-step 1024
"Print progress messages in these intervals.")
(defvar sqlplus-html-work-buffer)
(if (fboundp 'add-minor-mode)
(add-minor-mode 'sqlplus-html-mode " HTML")
(pushnew '(sqlplus-html-mode (" HTML")) minor-mode-alist
:test 'equal))
;;;#autoload
(defun sqlplus-html-init-all ()
"Put SQL*Plus in HTML mode and turn on sqlplus-html-mode.
This assumes you are in a shell buffer, running a SQL*Plus session."
(interactive)
(comint-send-string (current-buffer) "set linesize 10000\n")
(comint-send-string (current-buffer) "set pagesize 10000\n")
(comint-send-string (current-buffer) "set markup html on\n")
(sqlplus-html-mode 1))
;;;###autoload
(defun sqlplus-html-mode (&optional arg)
"Toggle sqlplus-html-mode.
When active, handle intercept SQL*Plus HTML output, render it using
`w3', and replace the original output with the rendered version.
This works by hijacking the process filter so that it calls our
`sqlplus-html-output-filter' instead of `comint-output-filter'. It also
sets `truncate-lines' to t and makes sure that the HTML accumulation
buffer is killed when the SQL*Plus buffer is killed or when the mode
is turned off."
(interactive)
;; XXX Should at least assert that the buffer is in a comint-derived
;; mode!
(setq sqlplus-html-mode
(cond ((eq arg t) t)
((null arg) (not sqlplus-html-mode))
((> (prefix-numeric-value arg) 0))))
(cond (sqlplus-html-mode
(make-local-hook 'kill-buffer-hook)
(add-hook 'kill-buffer-hook 'sqlplus-html-kill-work-buffer nil t)
(set-process-filter (get-buffer-process (current-buffer))
'sqlplus-html-output-filter)
(setq truncate-lines t))
(t
;; Clean up after a run of the mode.
(let ((process (get-buffer-process (current-buffer)))
(workbuf (and (boundp 'sqlplus-html-work-buffer)
(buffer-live-p sqlplus-html-work-buffer)
sqlplus-html-work-buffer)))
;; If there's any pending output, process it.
(when (and workbuf
(not (zerop (buffer-size workbuf))))
(comint-output-filter process (with-current-buffer workbuf
(buffer-string))))
(sqlplus-html-kill-work-buffer)
(remove-hook 'kill-buffer-hook 'sqlplus-html-kill-work-buffer t)
(set-process-filter process 'comint-output-filter))
(setq truncate-lines (default-value 'truncate-lines))))
(sqlplus-html-redraw-modeline))
;; This used to be a lambda, but I changed it to defun to make its
;; removal easier.
(defun sqlplus-html-kill-work-buffer ()
;; Use `ignore-errors' to avoid problems if somebody else killed the
;; working buffer, if sqlplus-html-work-buffer is still unbound, etc.
(ignore-errors
(kill-buffer sqlplus-html-work-buffer)))
(defun sqlplus-html-ensure-work-buffer ()
"Ensure that the HTML work buffer exists, and return it."
(let ((buf (and (boundp 'sqlplus-html-work-buffer)
(buffer-live-p sqlplus-html-work-buffer)
sqlplus-html-work-buffer)))
(if buf
buf
(set (make-local-variable 'sqlplus-html-work-buffer)
(generate-new-buffer " *sqlplus-html*"))
sqlplus-html-work-buffer)))
;; The workhorse of the module: accumulate subprocess output in a work
;; buffer until the prompt is encountered. Then feed the collected
;; HTML to a renderer and call `comint-output-filter' with the result.
;; That way comint "sees" the rendered HTML as originating from the
;; subprocess, which is what we want.
(defun sqlplus-html-output-filter (process output-string)
(let ((work-buffer (sqlplus-html-ensure-work-buffer))
(result nil))
(with-current-buffer work-buffer
;; Report the progress.
(sqlplus-html-receive-progress (buffer-size) (length output-string))
;; Append the output to the work buffer.
(goto-char (point-max))
(insert output-string)
;; Try to find the sqlplus prompt. We're now at point-max.
;; Back out 20 characters and then search for
;; sqlplus-html-prompt-regexp.
(ignore-errors
(backward-char 20))
(when (re-search-forward sqlplus-html-prompt-regexp nil t)
;; We found the prompt regexp. This means that the output
;; from the last command is finished and that we can process
;; it.
(sqlplus-html-process-region (point-min) (point-max))
(setq result (buffer-string))
;; Note: we're erasing the whole buffer here, not only the
;; part until the prompt we've matched. But that's ok because
;; sqlplus-html-prompt-regexp includes an end-of-buffer anchor
;; which ensures that if we're here, the whole buffer contents
;; should be displayed (and hence erased from the tmp buffer).
(erase-buffer)))
(when result
;; Clear the echo area.
(message "")
;; Pass the result string to comint so that it thinks the input
;; comes from the subprocess.
(comint-output-filter process result))))
(defun sqlplus-html-process-region (b e)
(save-restriction
(narrow-to-region b e)
(let ((fn (intern (format "sqlplus-html-process-impl-%s"
sqlplus-html-process-method))))
(funcall fn))))
(defun sqlplus-html-process-impl-w3 ()
(flet ((message (&rest ignored)
;; Disable `message' to avoid ugly "drawing..."
;; messages printed by w3.
t))
(w3-region (point-min) (point-max)))
;; Post-process w3 output.
(goto-char (point-min))
;; Delete leading newlines, except for the very first one.
(if (looking-at "\n\\(\n+\\)")
(delete-region (match-beginning 1) (match-end 1)))
;; Delete trailing newlines.
(goto-char (point-max))
(while (memq (char-before) '(?\n ?\r))
(delete-char -1)))
(defun sqlplus-html-process-impl-links ()
(let ((tmp (make-temp-name (expand-file-name
"sqlplus-html"
(sqlplus-html-temp-directory)))))
(write-region (point-min) (point-max) tmp nil 'silent)
(delete-region (point-min) (point-max))
(unwind-protect
;; It would be nice if we could use `call-process-region' to
;; feed the HTML to links's stdin thus avoiding the tmpfile.
;; But `links -dump /dev/stdin' doesn't work when stdin is a
;; pipe.
(call-process "links" nil t nil "-dump" tmp)
(delete-file tmp)))
;; Post-process links output.
(goto-char (point-min))
;; Delete the annoying three spaces preceding each line of links
;; output.
(while (re-search-forward "^ " nil t)
(delete-region (match-beginning 0) (match-end 0)))
;; Delete trailing newlines.
(goto-char (point-max))
(while (memq (char-before) '(?\n ?\r))
(delete-char -1)))
(defun sqlplus-html-process-impl-w3m ()
;; w3m can read from stdin, hence we don't need temporary files.
(call-process-region (point-min) (point-max) "w3m" t t nil
"-dump" "-T" "text/html")
;; Delete trailing newlines.
(goto-char (point-max))
(while (memq (char-before) '(?\n ?\r))
(delete-char -1)))
(defun sqlplus-html-receive-progress (oldtotal new)
(let ((newtotal (+ oldtotal new)))
(when (> newtotal sqlplus-html-receive-progress-threshold)
(let ((old-count (/ oldtotal 1024))
(new-count (/ newtotal 1024)))
(when (> new-count old-count)
(message "sqlplus read: %dk" (/ newtotal 1024)))))))
;;; Cross-Emacs compatibility.
(defun sqlplus-html-temp-directory ()
(or (and (fboundp 'temp-directory)
(temp-directory))
(getenv "TMPDIR")
"/tmp"))
(defun sqlplus-html-redraw-modeline ()
(cond ((fboundp 'redraw-modeline)
(redraw-modeline))
((fboundp 'force-mode-line-update)
(force-mode-line-update))
(t
;; Oldie-goldie.
(set-buffer-modified-p (buffer-modified-p)))))
(provide 'sqlplus-html)
;;; sqlplus-html.el ends here