xwidget-plus-follow-link.el 14 KB

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