;;; shr-url.el --- Speech-enable SHR
;;; $Id: emacspeak-shr.el 4797 2007-07-16 23:31:22Z tv.raman.tv $
;;; $Author: tv.raman.tv $
;;; Description:  Speech-enable SHR An Emacs Interface to shr
;;; Keywords: Emacspeak,  Audio Desktop shr
;;{{{  LCD Archive entry:

;;; LCD Archive Entry:
;;; emacspeak| T. V. Raman |raman@cs.cornell.edu
;;; A speech interface to Emacs |
;;; $Date: 2007-05-03 18:13:44 -0700 (Thu, 03 May 2007) $ |
;;;  $Revision: 4532 $ |
;;; Location undetermined
;;;

;;}}}
;;{{{  Copyright:
;;;Copyright (C) 1995 -- 2007, 2011, T. V. Raman
;;; Copyright (c) 1994, 1995 by Digital Equipment Corporation.
;;; All Rights Reserved.
;;;
;;; This file is not part of GNU Emacs, but the same permissions apply.
;;;
;;; GNU Emacs 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.
;;;
;;; GNU Emacs is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNSHR 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 GNU Emacs; see the file COPYING.  If not, write to
;;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

;;}}}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;{{{  introduction

;;; Commentary:
;;; SHR ==  Simple HTML  Reader
;;; Code:

;;}}}
;;{{{  Required modules

(require 'cl)
(require 'url-parse)
(declaim  (optimize  (safety 0) (speed 3)))
(require 'emacspeak-preamble)
(require 'shr)
(require 'xml)
;;}}}
;;{{{ Enhanced shr:

(defsubst shr-url-get-title-from-dom (dom)
  "Return Title."
  (let ((content dom)
        (title nil))
    (while
        (and content
             (listp content)
             (not
              (setq title
                    (find-if
                     #'(lambda (e) (and (listp e)(eq 'title (car
                                                             e))))
                     (third content)))))
      (setq content (third content)))
    (when title (third title))))
(defsubst shr-url-get-link-text()
  "Return link text at point."
  (let ((url (get-text-property (point) 'shr-url))
        (start nil)
        (end nil))
    (cond
     ((null url)  nil)
     (t
      (setq start (previous-single-property-change (point) 'shr-url)
            end (next-single-property-change (point) 'shr-url))
      (buffer-substring start end)))))

(defvar shr-url-dom nil
  "Buffer local value of DOM.")
(make-variable-buffer-local 'shr-url-dom)

(declaim (special shr-map))
(when (and (boundp 'shr-map) shr-map)
  (loop for k in
        '(
          ("\C-i" shr-url-next-link)
          ("a" shr-url-view-filtered-dom-by-attribute)
          ("e" shr-url-view-filtered-dom-by-element-list)
          ("o" shr-url-open-link-at-point)
          ([backtab] shr-url-previous-link)
          ("\M-\C-i" shr-url-previous-link)
          ("q" bury-buffer)
          )
        do
        (emacspeak-keymap-update  shr-map  k)))
(defvar shr-url-this-url nil
  "Buffer local copy of URL of current page.")

(make-variable-buffer-local 'shr-url-this-url)

(defun shr-url-callback (status &rest args)
  "Callback for url-retrieve.
URL  being retrieved is received as part of the callback args."
  (declare (special  shr-map shr-url-dom shr-url-this-url))
  (goto-char (point-min))
  (let*
      ((inhibit-read-only t)
       (start (re-search-forward "^$"))
       (dom (libxml-parse-html-region start(point-max)))
       (buffer (get-buffer-create (or (shr-url-get-title-from-dom dom) "Web"))))
    (with-current-buffer buffer
      (erase-buffer)
      (special-mode)
      (shr-insert-document dom)
      (setq shr-url-dom dom
            shr-url-this-url (first args))
      (goto-char (point-min))
      (set-buffer-modified-p nil)
      (flush-lines "^ *$")
      (use-local-map shr-map)
      (setq buffer-read-only t))
    (switch-to-buffer buffer)
    (emacspeak-auditory-icon 'open-object)
    (emacspeak-speak-buffer)))

;;;###autoload
(defun shr-url (url &optional display)
  "Display web page."
  (interactive
   (list
    (read-from-minibuffer "URL: "
                          (get-text-property (point) 'shr-url))
    current-prefix-arg))
  (url-retrieve url 'shr-url-callback (list url)))

;;;###autoload

(defun shr-url-open-link-at-point ()
  "Open link under point using shr."
  (interactive)
  (let ((url (get-text-property (point) 'shr-url)))
    (cond
     ((null url) (message "Not on a link."))
     (shr-base (shr-url (shr-expand-url url)))
     (t (shr-url url)))))

;;;###autoload
(defun shr-url-region (start end)
  "Display region as web page."
  (interactive "r")
  (let* ((inhibit-read-only t)
         (dom (libxml-parse-html-region start end))
         (buffer
          (get-buffer-create
           (or
            (shr-url-get-title-from-dom dom)
            "Untitled"))))
    (with-current-buffer buffer
      (erase-buffer)
      (shr-insert-document dom)
      (setq shr-url-dom dom)
      (goto-char (point-min))
      (set-buffer-modified-p nil)
      (flush-lines "^ *$")
      (setq buffer-read-only t))
    (switch-to-buffer buffer)
    (emacspeak-speak-mode-line)))

(defun shr-url-next-link ()
  "Move to next link."
  (interactive)
  (let ((url (get-text-property (point) 'shr-url)))
    (when url (goto-char (next-single-property-change (point) 'shr-url)))
    (setq url (next-single-property-change (point) 'shr-url)); find next link
    (when url (goto-char url))))

(defun shr-url-previous-link ()
  "Move to previous link."
  (interactive)
  (let ((url (get-text-property (point) 'shr-url)))
    (when url (goto-char (previous-single-property-change (point) 'shr-url)))
    (setq url (previous-single-property-change (point) 'shr-url)); find next link
    (when url (goto-char url))))

;;}}}
;;{{{ class and id caches:
(defvar shr-url-cache-updated nil
  "Records if caches are updated.")

(make-variable-buffer-local 'shr-url-cache-updated)

(defvar shr-url-id-cache nil
  "Cache of id values. Is buffer-local.")
(make-variable-buffer-local 'shr-url-id-cache)

(defvar shr-url-class-cache nil
  "Cache of class values. Is buffer-local.")
(make-variable-buffer-local 'shr-url-class-cache)

(defvar shr-url-element-cache nil
  "Cache of element names. Is buffer-local.")
(make-variable-buffer-local 'shr-url-element-cache)

(defun shr-url-update-cache (dom)
  "Update element, class and id cache."
  (declare (special shr-url-element-cache shr-url-id-cache
                    shr-url-class-cache shr-url-cache-updated))
  (when (listp dom)                         ; build cache
    (let ((id (xml-get-attribute-or-nil dom 'id))
          (class (xml-get-attribute-or-nil dom 'class))
          (el (symbol-name (xml-node-name dom)))
          (children (xml-node-children dom)))
      (when id (pushnew  id shr-url-id-cache))
      (when class (pushnew class shr-url-class-cache))
      (when el (pushnew el shr-url-element-cache))
      (when children (mapc #'shr-url-update-cache children)))))

;;}}}
;;{{{ Filter DOM:

(defun shr-url-filter-dom (dom predicate)
  "Return DOM dom filtered by predicate.
  Predicate receives the node to test."
  (cond
   ((not (listp dom)) nil)
   ((funcall predicate dom) dom)
   (t
    (let ((filtered (delq nil (mapcar
                               #'(lambda (node)
                                   (shr-url-filter-dom node predicate))
                               (xml-node-children dom)))))
      (when filtered
        (push (xml-node-attributes dom) filtered)
        (push (xml-node-name dom) filtered))))))

(defun shr-url-attribute-tester (attr value)
  "Return predicate that tests for attr=value for use as  a DOM filter."
  (eval
   `(defun ,(gensym "shr-url-predicate") (node)
      ,(format "Test if attribute %s has value %s" attr value)
      (when
          (equal (xml-get-attribute node (quote ,attr)) ,value) node))))

(defun shr-url-elements-tester (element-list)
  "Return predicate that tests for presence of element in element-list for use as  a DOM filter."
  (eval
   `(defun ,(gensym "shr-url-predicate") (node)
      ,(format "Test if node  is a member of  %s" element-list)
      (when (member (xml-node-name node) (quote ,element-list)) node))))

;;{{{ Speech-enable:

(loop for f in
      '(shr-url-next-link shr-url-previous-link)
      do
      (eval
       `(defadvice ,f (after emacspeak pre act comp)
          "Provide auditory feedback."
          (when (ems-interactive-p)
            (emacspeak-auditory-icon 'large-movement)
            (and (get-text-property (point) 'shr-url)
                 (message (shr-url-get-link-text)))))))

;;}}}

(defun shr-url-view-filtered-dom-by-attribute ()
  "Display DOM filtered by specified attribute=value test."
  (interactive)
  (declare (special shr-url-id-cache shr-url-class-cache
                    shr-url-this-url shr-url-cache-updated shr-dom shr-map))
  (unless (and (boundp 'shr-url-dom) shr-url-dom) (error "No DOM to filter!"))
  (unless shr-url-cache-updated
    (shr-url-update-cache shr-url-dom)
    (setq shr-url-cache-updated t))
  (unless (or shr-url-id-cache shr-url-class-cache) (error "No id/class to filter."))
  (let*
      ((attr (read (completing-read "Attribute: " '("id" "class"))))
       (value (completing-read "Value: " (if (eq attr 'id) shr-url-id-cache shr-url-class-cache)))
       (buffer nil)
       (inhibit-read-only t)
       (url (url-generic-parse-url shr-url-this-url))
       (dom
        (shr-url-filter-dom shr-url-dom (shr-url-attribute-tester attr value))))
    (when dom
      (setq buffer (get-buffer-create "SHR Filtered"))
      (with-current-buffer buffer
        (erase-buffer)
        (goto-char (point-min))
        (special-mode)
        (shr-insert-document dom)
        (setq shr-base
              (concat
               (url-type url)
               "://"
               (url-host url)
               (file-name-directory (url-filename url))))
        (rename-buffer (or (shr-url-get-title-from-dom dom) "Filtered")'unique)
        (setq shr-url-dom dom)
        (set-buffer-modified-p nil)
        (flush-lines "^ *$")
        (use-local-map shr-map)
        (setq buffer-read-only t))
      (switch-to-buffer buffer)
      (emacspeak-auditory-icon 'open0-object)
      (emacspeak-speak-buffer))))

(defun shr-url-view-filtered-dom-by-element-list ()
  "Display DOM filtered by specified el list."
  (interactive)
  (declare (special shr-url-element-cache
                    shr-url-this-url shr-url-cache-updated shr-dom shr-map))
  (unless (and (boundp 'shr-url-dom) shr-url-dom) (error "No DOM to filter!"))
  (unless shr-url-cache-updated
    (shr-url-update-cache shr-url-dom)
    (setq shr-url-cache-updated t))
  (let ((el-list nil)
        (el  (completing-read "Element: " shr-url-element-cache)))
    (loop until (zerop (length  el))
          do
          (pushnew (read el)  el-list)
          (setq el  (completing-read "Element: " shr-url-element-cache)))
    (let
        ((buffer nil)
         (url (url-generic-parse-url shr-url-this-url))
         (inhibit-read-only t)
         (dom (shr-url-filter-dom shr-url-dom (shr-url-elements-tester el-list))))
      (when dom
        (setq buffer (get-buffer-create "SHR Filtered"))
        (with-current-buffer buffer
          (erase-buffer)
          (goto-char (point-min))
          (special-mode)
          (shr-insert-document dom)
          (setq shr-base
                (concat
                 (url-type url)
                 "://"
                 (url-host url)
                 (file-name-directory (url-filename url))))
          (rename-buffer (or (shr-url-get-title-from-dom dom) "Filtered")'unique)
          (setq shr-url-dom dom)
          (set-buffer-modified-p nil)
          (flush-lines "^ *$")
          (use-local-map shr-map)
          (setq buffer-read-only t))
        (switch-to-buffer buffer)
        (emacspeak-auditory-icon 'open-object)
        (emacspeak-speak-buffer)))))

;;}}}
(provide 'emacspeak-shr)
;;{{{ end of file

;;; local variables:
;;; folded-file: t
;;; byte-compile-dynamic: nil
;;; end:

;;}}}
