xwidget-plus.el 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. ;;; xwidget-plus.el --- Improve xwidget usability
  2. ;; Author: Damien Merenne
  3. ;; URL: https://github.com/canatella/xwidget-plus
  4. ;; Created: 2020-03-11
  5. ;; Keywords: convenience
  6. ;; Version: 0.1
  7. ;; Package-Requires: ((emacs "25.3") (ivy "0.13.0"))
  8. ;; This file is NOT part of GNU Emacs.
  9. ;;; Commentary:
  10. ;; This package augment the xwidget-webkit browser to make it more usable.
  11. ;;; License:
  12. ;; This program is free software; you can redistribute it and/or modify
  13. ;; it under the terms of the GNU General Public License as published by
  14. ;; the Free Software Foundation; either version 3, or (at your option)
  15. ;; any later version.
  16. ;;
  17. ;; This program is distributed in the hope that it will be useful,
  18. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. ;; GNU General Public License for more details.
  21. ;;
  22. ;; You should have received a copy of the GNU General Public License
  23. ;; along with GNU Emacs; see the file COPYING. If not, write to the
  24. ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  25. ;; Boston, MA 02110-1301, USA.
  26. ;;; Code:
  27. ;;; User customizable variable
  28. ;;
  29. ;;; Code:
  30. (require 'xwidget)
  31. (require 'ivy)
  32. (defgroup xwidget-plus nil
  33. "Augment the xwidget webkit browser."
  34. :group 'convenience)
  35. (defcustom xwidget-plus-follow-link-candidate-style '(("border" . "1px dashed blue")
  36. ("background" . "#0000ff20"))
  37. "Style to apply to candidate links."
  38. :type '(list (cons string string))
  39. :group 'xwidget-plus)
  40. (defcustom xwidget-plus-follow-link-selected-style '(("border" . "1px dashed red")
  41. ("background" . "#ff000020"))
  42. "Style to apply to currently selected link."
  43. :type '(list (cons string string))
  44. :group 'xwidget-plus)
  45. ;; Bring the window to front when summoning browse
  46. (defun xwidget-plus-webkit-browse-url-advise (&rest _)
  47. "Advice to add switch to window when calling `xwidget-webkit-browse-url'."
  48. (switch-to-buffer-other-window (xwidget-buffer (xwidget-webkit-current-session))))
  49. (advice-add #'xwidget-webkit-browse-url :after #'xwidget-plus-webkit-browse-url-advise)
  50. (defun xwidget-plus-make-class (class style)
  51. "Generate a css CLASS definition from the STYLE alist."
  52. (format ".%s { %s }\\n" class (mapconcat (lambda (v) (format "%s: %s;" (car v) (cdr v))) style " ")))
  53. (defun xwidget-plus-follow-link-style-definition ()
  54. "Return the css definitions for the follow link feature."
  55. (concat (xwidget-plus-make-class "xwidget-plus-follow-link-candidate" xwidget-plus-follow-link-candidate-style)
  56. (xwidget-plus-make-class "xwidget-plus-follow-link-selected" xwidget-plus-follow-link-selected-style)))
  57. (defun xwidget-plus-follow-link-format-link (str)
  58. "Format link title STR."
  59. (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
  60. (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
  61. (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
  62. (replace-regexp-in-string "[[:space:]]+" " " str))
  63. (defmacro --js (js _ &rest replacements)
  64. "Apply `format' on JS with REPLACEMENTS providing MMM mode delimiters.
  65. This file has basic support for javascript using MMM mode and
  66. local variables (see at the end of the file)."
  67. (declare (indent 3))
  68. `(format ,js ,@replacements))
  69. (defun xwidget-plus-js-string-escape (string)
  70. "Escape STRING for injection."
  71. (replace-regexp-in-string "\n" "\\\\n" (replace-regexp-in-string "'" "\\\\'" string)))
  72. (defun xwidget-plus-inject-head-element (xwidget tag id type content)
  73. "Insert TAG element under XWIDGET head with ID TYPE and CONTENT."
  74. (let* ((id (xwidget-plus-js-string-escape id))
  75. (tag (xwidget-plus-js-string-escape tag))
  76. (type (xwidget-plus-js-string-escape type))
  77. (content (xwidget-plus-js-string-escape content))
  78. (script (--js "
  79. __xwidget_id = '%s';
  80. if (!document.getElementById(__xwidget_id)) {
  81. var e = document.createElement('%s');
  82. e.type = '%s';
  83. e.id = __xwidget_id;
  84. e.innerHTML = '%s';
  85. document.getElementsByTagName('head')[0].appendChild(e);
  86. };
  87. " js-- id tag type content)))
  88. (xwidget-webkit-execute-script xwidget script)))
  89. (defun xwidget-plus-inject-script (xwidget id script)
  90. "Inject javascript SCRIPT in XWIDGET session using a script element with ID."
  91. (xwidget-plus-inject-head-element xwidget "script" id "text/javascript" script))
  92. (defun xwidget-plus-inject-style (xwidget id style)
  93. "Inject css STYLE in XWIDGET session using a style element with ID."
  94. (xwidget-plus-inject-head-element xwidget "style" id "text/css" style))
  95. (defconst xwidget-plus-follow-link-script (--js "
  96. function __xwidget_plus_follow_link_cleanup() {
  97. document.querySelectorAll('a').forEach(a => {
  98. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  99. });
  100. }
  101. function __xwidget_plus_follow_link_highlight(json, selected) {
  102. var ids = JSON.parse(json);
  103. document.querySelectorAll('a').forEach((a, id) => {
  104. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  105. if (selected == id) {
  106. a.classList.add('xwidget-plus-follow-link-selected');
  107. a.scrollIntoView({behavior: 'smooth', block: 'center'});
  108. } else if (ids[id]) {
  109. a.classList.add('xwidget-plus-follow-link-candidate');
  110. }
  111. });
  112. }
  113. function __xwidget_plus_follow_link_action(id) {
  114. __xwidget_plus_follow_link_cleanup();
  115. document.querySelectorAll('a')[id].click();
  116. }
  117. function __xwidget_plus_follow_link_links() {
  118. var r = {};
  119. document.querySelectorAll('a').forEach((a, i) => {
  120. if (a.offsetWidth || a.offsetHeight || a.getClientRects().length)
  121. r[i] = a.innerText;
  122. });
  123. return r;
  124. }
  125. " js--))
  126. (defun xwidget-plus-follow-link-highlight (xwidget links)
  127. "Highligh LINKS in XWIDGET buffer when updating ivy candidates."
  128. (with-current-buffer (ivy-state-buffer ivy-last)
  129. (let* ((cands (ivy--filter ivy-text ivy--all-candidates))
  130. (selected (ivy-state-current ivy-last))
  131. (cands-id (seq-filter (lambda (v) (seq-contains-p cands (cdr v))) links))
  132. (selected-id (car (rassoc selected links)))
  133. (script (--js "__xwidget_plus_follow_link_highlight('%s', '%s');" js-- (json-serialize cands-id) selected-id)))
  134. (xwidget-webkit-execute-script xwidget script))))
  135. (defun xwidget-plus-follow-link-exit (xwidget)
  136. "Exit follow link mode in XWIDGET."
  137. (let ((script "__xwidget_plus_follow_link_cleanup();"))
  138. (xwidget-webkit-execute-script xwidget script)))
  139. (defun xwidget-plus-follow-link-action (xwidget links selected)
  140. "Activate link matching SELECTED in XWIDGET LINKS."
  141. (let* ((selected-id (car (rassoc selected links)))
  142. (script (--js "__xwidget_plus_follow_link_action('%s');" js-- selected-id)))
  143. (xwidget-webkit-execute-script xwidget script)))
  144. (defun xwidget-plus-follow-link-prepare-links (links)
  145. "Prepare the alist of LINKS."
  146. (setq links (seq-sort-by (lambda (v) (string-to-number (car v))) #'< links))
  147. (setq links (seq-map (lambda (v) (cons (intern (car v)) (xwidget-plus-follow-link-format-link (cdr v))))
  148. links))
  149. (seq-filter #'identity links))
  150. (defun xwidget-plus-follow-link-callback (links)
  151. "Ask for a link belonging to the alist LINKS."
  152. (let* ((xwidget (xwidget-webkit-current-session))
  153. (links (xwidget-plus-follow-link-prepare-links links))
  154. (choice (seq-map #'cdr links)))
  155. (unwind-protect
  156. (ivy-read "Link: " choice
  157. :action (apply-partially #'xwidget-plus-follow-link-action xwidget links)
  158. :update-fn (apply-partially #'xwidget-plus-follow-link-highlight xwidget links))
  159. (xwidget-plus-follow-link-exit xwidget))))
  160. ;;;###autoload
  161. (defun xwidget-plus-follow-link (&optional xwidget)
  162. "Ask for a link in the XWIDGET session or the current one and follow it."
  163. (interactive)
  164. (let ((xwidget (or xwidget (xwidget-webkit-current-session)))
  165. (script (--js "__xwidget_plus_follow_link_links();" js--)))
  166. (xwidget-plus-inject-style xwidget "__xwidget_plus_follow_link_style" (xwidget-plus-follow-link-style-definition))
  167. (xwidget-plus-inject-script xwidget "__xwidget_plus_follow_link_script" xwidget-plus-follow-link-script)
  168. (xwidget-webkit-execute-script xwidget script #'xwidget-plus-follow-link-callback)))
  169. (provide 'xwidget-link)
  170. ;;; xwidget-link.el ends here
  171. ;; Local Variables:
  172. ;; eval: (mmm-mode)
  173. ;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
  174. ;; mmm-classes: elisp-js
  175. ;; End: