xwidget-plus-follow-link.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. ;;; xwidget-plus-follow-link.el --- Link navigation in browsers -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2020 Damien Merenne <dam@cosinux.org>
  3. ;; This file is NOT part of GNU Emacs.
  4. ;;; Commentary:
  5. ;; Add support for navigating web pages using the minibuffer completion.
  6. ;;; License:
  7. ;; This program is free software: you can redistribute it and/or modify
  8. ;; it under the terms of the GNU General Public License as published by
  9. ;; the Free Software Foundation, either version 3 of the License, or
  10. ;; (at your option) any later version.
  11. ;; This program is distributed in the hope that it will be useful,
  12. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;; GNU General Public License for more details.
  15. ;; You should have received a copy of the GNU General Public License
  16. ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. ;;; Commentary:
  18. ;;
  19. ;;; Code:
  20. (require 'xwidget)
  21. (require 'xwidget-plus-common)
  22. (require 'eieio)
  23. (require 'cl-lib)
  24. (defcustom xwidget-plus-follow-link-candidate-style '(("border" . "1px dashed blue")
  25. ("background" . "#0000ff20"))
  26. "Style to apply to candidate links."
  27. :type '(list (cons string string))
  28. :group 'xwidget-plus)
  29. (defcustom xwidget-plus-follow-link-selected-style '(("border" . "1px dashed red")
  30. ("background" . "#ff000020"))
  31. "Style to apply to currently selected link."
  32. :type '(list (cons string string))
  33. :group 'xwidget-plus)
  34. (defun xwidget-plus-follow-link-style-definition ()
  35. "Return the css definitions for the follow link feature."
  36. (concat (xwidget-plus-make-class "xwidget-plus-follow-link-candidate" xwidget-plus-follow-link-candidate-style)
  37. (xwidget-plus-make-class "xwidget-plus-follow-link-selected" xwidget-plus-follow-link-selected-style)))
  38. (xwidget-plus-js-def follow-link cleanup ()
  39. "Remove all custom class from links.""
  40. document.querySelectorAll('a').forEach(a => {
  41. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  42. });
  43. ")
  44. (xwidget-plus-js-def follow-link highlight (ids selected)
  45. "Highlight IDS as candidate and SELECTED as selected.""
  46. document.querySelectorAll('a').forEach((a, id) => {
  47. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  48. if (selected == id) {
  49. a.classList.add('xwidget-plus-follow-link-selected');
  50. a.scrollIntoView({behavior: 'smooth', block: 'center'});
  51. } else if (ids && ids.includes(id)) {
  52. a.classList.add('xwidget-plus-follow-link-candidate');
  53. }
  54. });
  55. ")
  56. (xwidget-plus-js-def follow-link action (link-id)
  57. "Click on the link identified by LINK-ID""
  58. __xwidget_plus_follow_link_cleanup();
  59. document.querySelectorAll('a')[link_id].click();
  60. ")
  61. (xwidget-plus-js-def follow-link fetch-links ()
  62. "Fetch all visible, non empty links from the current page.""
  63. var r = {};
  64. document.querySelectorAll('a').forEach((a, i) => {
  65. if (a.offsetWidth || a.offsetHeight || a.getClientRects().length) {
  66. if (a.innerText.match(/\\\\S/))
  67. r[i] = a.innerText;
  68. }
  69. });
  70. return r;
  71. ")
  72. ;; Completion backend class
  73. (defclass xwidget-plus-completion-backend () ((collection) (text)))
  74. (cl-defmethod xwidget-plus-follow-link-candidates ((_backend xwidget-plus-completion-backend))
  75. "Return the BACKEND selected link and the candidates.
  76. The return value is a list whose first element is the selected id
  77. link and the rest are the candidates ids.
  78. Return nil if the backend does not support narrowing selection list.")
  79. (cl-defmethod xwidget-plus-follow-link-read ((_backend xwidget-plus-completion-backend)
  80. _prompt _collection _action _update-fn)
  81. "Use BACKEND to PROMPT the user for a link in COLLECTION.
  82. ACTION should be called with the resulting link.
  83. UPDATE-FN is a function that can be called when the candidates
  84. list is narrowed.It will highlight the link list in the
  85. browser.")
  86. ;; Default backend using completing-read
  87. (defclass xwidget-plus-completion-backend-default (xwidget-plus-completion-backend) ())
  88. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-default))
  89. "Return the BACKEND selected link and the candidates.
  90. The return value is a list whose first element is the selected id
  91. link and the rest are the candidates ids.
  92. Return nil if the backend does not support narrowing selection list."
  93. (let* ((collection (oref backend collection))
  94. (text (oref backend text))
  95. (matches (seq-filter (lambda (i) (string-match-p (concat "^" (regexp-quote text)) (car i))) collection))
  96. (matches (seq-map #'cdr matches)))
  97. (if (= 1 (length matches))
  98. matches
  99. (cons nil matches))))
  100. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-default) prompt collection action update-fn)
  101. "Use BACKEND to PROMPT the user for a link in COLLECTION.
  102. ACTION should be called with the resulting link.
  103. UPDATE-FN is a function that can be called when the candidates
  104. list is narrowed.It will highlight the link list in the
  105. browser."
  106. (funcall action (cdr (assoc (completing-read prompt (lambda (str pred _)
  107. (oset backend text str)
  108. (funcall update-fn)
  109. (try-completion str collection pred))
  110. nil t)
  111. collection))))
  112. ;; Ido backend using ido-completing-read
  113. (with-eval-after-load 'ido
  114. ;; tell the compiler these do exists
  115. (defvar ido-matches)
  116. (declare-function ido-set-matches "ido")
  117. (defclass xwidget-plus-completion-backend-ido (xwidget-plus-completion-backend) ())
  118. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ido))
  119. (let ((collection (oref backend collection)))
  120. (when collection
  121. (seq-map (lambda (i) (cdr (assoc i collection))) ido-matches))))
  122. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ido) prompt collection action update-fn)
  123. (let ((choices (seq-map #'car collection)))
  124. (advice-add #'ido-set-matches :after update-fn)
  125. (let ((link (unwind-protect
  126. (cdr (assoc (ido-completing-read prompt choices nil t) collection))
  127. (oset backend collection nil)
  128. (advice-remove #'ido-set-matches update-fn))))
  129. (funcall action link)))))
  130. ;; Ivy backend using completing read
  131. (with-eval-after-load 'ivy
  132. ;; tell the compiler these do exists
  133. (defvar ivy-last)
  134. (defvar ivy-text)
  135. (defvar ivy--all-candidates)
  136. (declare-function ivy-read "ivy")
  137. (declare-function ivy-state-buffer "ivy")
  138. (declare-function ivy-state-collection "ivy")
  139. (declare-function ivy-state-current "ivy")
  140. (declare-function ivy--filter "ivy")
  141. (defclass xwidget-plus-completion-backend-ivy (xwidget-plus-completion-backend) ())
  142. (cl-defmethod xwidget-plus-follow-link-candidates ((_ xwidget-plus-completion-backend-ivy))
  143. (with-current-buffer (ivy-state-buffer ivy-last)
  144. (let* ((collection (ivy-state-collection ivy-last))
  145. (current (ivy-state-current ivy-last))
  146. (candidates (ivy--filter ivy-text ivy--all-candidates))
  147. (result (cons current candidates)))
  148. (seq-map (lambda (c) (cdr (nth (get-text-property 0 'idx c) collection))) result))))
  149. (cl-defmethod xwidget-plus-follow-link-read ((_ xwidget-plus-completion-backend-ivy) prompt collection action update-fn)
  150. (ivy-read prompt collection :require-match t :action (lambda (v) (funcall action (cdr v))) :update-fn update-fn)))
  151. ;; Helm backend
  152. (with-eval-after-load 'helm
  153. ;; tell the compiler these do exists
  154. (declare-function helm "helm")
  155. (declare-function helm-get-selection "helm")
  156. (declare-function helm-make-source "helm-source")
  157. (defclass xwidget-plus-completion-backend-helm (xwidget-plus-completion-backend) ((candidates)))
  158. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-helm))
  159. (let* ((candidates (oref backend candidates))
  160. (selection (helm-get-selection))
  161. (selected (when selection (cdr (elt (oref backend collection) selection))))
  162. (result (seq-map #'cdr candidates)))
  163. (cons selected result)))
  164. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-helm) prompt collection action update-fn)
  165. (add-hook 'helm-after-initialize-hook (lambda ()
  166. (with-current-buffer "*helm-xwidget-plus*"
  167. (add-hook 'helm-move-selection-after-hook update-fn nil t)))
  168. nil t)
  169. (helm :sources
  170. (helm-make-source "Xwidget Plus" 'helm-source-sync
  171. :candidates collection
  172. :action action
  173. :filtered-candidate-transformer (lambda (candidates _)
  174. (oset backend candidates candidates)
  175. (funcall update-fn)
  176. candidates))
  177. :prompt prompt
  178. :buffer "*helm-xwidget-plus*")))
  179. ;; Tell the compiler that the backend function exists
  180. (declare-function xwidget-plus-completion-backend-ido "xwidget-plus-follow-link")
  181. (declare-function xwidget-plus-completion-backend-ido--eieio-childp "xwidget-plus-follow-link")
  182. (declare-function xwidget-plus-completion-backend-ivy "xwidget-plus-follow-link")
  183. (declare-function xwidget-plus-completion-backend-ivy--eieio-childp "xwidget-plus-follow-link")
  184. (declare-function xwidget-plus-completion-backend-helm "xwidget-plus-follow-link")
  185. (declare-function xwidget-plus-completion-backend-helm--eieio-childp "xwidget-plus-follow-link")
  186. (defun xwidget-plus-follow-link-make-backend ()
  187. "Instanciate a completion backend."
  188. (cond ((eq xwidget-plus-completion-system 'default)
  189. (cond ((featurep 'ivy)
  190. #'xwidget-plus-completion-backend-ivy)
  191. ((featurep 'helm)
  192. #'xwidget-plus-completion-backend-helm)
  193. ((featurep 'ido)
  194. #'xwidget-plus-completion-backend-ido)
  195. (t #'xwidget-plus-completion-backend-default)))
  196. ((eq xwidget-plus-completion-system 'ivy)
  197. #'xwidget-plus-completion-backend-ivy)
  198. ((eq xwidget-plus-completion-system 'helm)
  199. #'xwidget-plus-completion-backend-helm)
  200. ((eq xwidget-plus-completion-system 'ido)
  201. #'xwidget-plus-completion-backend-ido)
  202. ((eq xwidget-plus-completion-system 'default)
  203. #'xwidget-plus-completion-backend-default)
  204. (t xwidget-plus-completion-system)))
  205. (defvar xwidget-plus-follow-link-completion-backend-instance '())
  206. (defun xwidget-plus-follow-link-update (xwidget)
  207. "Highligh LINKS in XWIDGET buffer when updating candidates."
  208. (let ((links (xwidget-plus-follow-link-candidates xwidget-plus-follow-link-completion-backend-instance)))
  209. (when links
  210. (let* ((selected (car links))
  211. (candidates (cdr links)))
  212. (xwidget-plus-follow-link-highlight xwidget candidates selected)))))
  213. (defun xwidget-plus-follow-link-trigger-action (xwidget selected)
  214. "Activate link matching SELECTED in XWIDGET LINKS."
  215. (xwidget-plus-follow-link-action xwidget selected))
  216. (defun xwidget-plus-follow-link-format-link (str)
  217. "Format link title STR."
  218. (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
  219. (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
  220. (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
  221. (replace-regexp-in-string "[[:space:]]+" " " str))
  222. (defun xwidget-plus-follow-link-prepare-links (links)
  223. "Prepare the alist of LINKS."
  224. (seq-sort-by (lambda (v) (cdr v)) #'<
  225. (seq-map (lambda (v) (cons (xwidget-plus-follow-link-format-link (cdr v)) (string-to-number (car v))))
  226. links)))
  227. (defun xwidget-plus-follow-link-callback (links)
  228. "Ask for a link belonging to the alist LINKS."
  229. (let* ((xwidget (xwidget-webkit-current-session))
  230. (links (xwidget-plus-follow-link-prepare-links links)))
  231. (oset xwidget-plus-follow-link-completion-backend-instance collection links)
  232. (unwind-protect
  233. (condition-case nil
  234. (xwidget-plus-follow-link-read xwidget-plus-follow-link-completion-backend-instance
  235. "Link: " links
  236. (apply-partially #'xwidget-plus-follow-link-trigger-action xwidget)
  237. (apply-partially #'xwidget-plus-follow-link-update xwidget))
  238. (quit (xwidget-plus-follow-link-cleanup xwidget))))
  239. (oset xwidget-plus-follow-link-completion-backend-instance collection nil)))
  240. ;;;###autoload
  241. (defun xwidget-plus-follow-link (&optional xwidget)
  242. "Ask for a link in the XWIDGET session or the current one and follow it."
  243. (interactive)
  244. (setq xwidget-plus-follow-link-completion-backend-instance (funcall (xwidget-plus-follow-link-make-backend)))
  245. (let ((xwidget (or xwidget (xwidget-webkit-current-session))))
  246. (xwidget-plus-inject-style xwidget "__xwidget_plus_follow_link_style" (xwidget-plus-follow-link-style-definition))
  247. (xwidget-plus-js-inject xwidget 'follow-link)
  248. (xwidget-plus-follow-link-fetch-links xwidget #'xwidget-plus-follow-link-callback)))
  249. ;; Local Variables:
  250. ;; eval: (mmm-mode)
  251. ;; eval: (mmm-add-group 'elisp-js '((elisp-rawjs :submode js-mode
  252. ;; :face mmm-code-submode-face
  253. ;; :delimiter-mode nil
  254. ;; :front "xwidget-plus--js \"" :back "\" js--")
  255. ;; (elisp-defjs :submode js-mode
  256. ;; :face mmm-code-submode-face
  257. ;; :delimiter-mode nil
  258. ;; :front "xwidget-plus-js-def .*\n.*\"\"\n" :back "\")\n")))
  259. ;; mmm-classes: elisp-js
  260. ;; End:
  261. (provide 'xwidget-plus-follow-link)
  262. ;;; xwidget-plus-follow-link.el ends here