xwidget-plus-follow-link.el 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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 'ivy)
  19. (require 'eieio)
  20. (defcustom xwidget-plus-follow-link-candidate-style '(("border" . "1px dashed blue")
  21. ("background" . "#0000ff20"))
  22. "Style to apply to candidate links."
  23. :type '(list (cons string string))
  24. :group 'xwidget-plus)
  25. (defcustom xwidget-plus-follow-link-selected-style '(("border" . "1px dashed red")
  26. ("background" . "#ff000020"))
  27. "Style to apply to currently selected link."
  28. :type '(list (cons string string))
  29. :group 'xwidget-plus)
  30. (defun xwidget-plus-follow-link-style-definition ()
  31. "Return the css definitions for the follow link feature."
  32. (concat (xwidget-plus-make-class "xwidget-plus-follow-link-candidate" xwidget-plus-follow-link-candidate-style)
  33. (xwidget-plus-make-class "xwidget-plus-follow-link-selected" xwidget-plus-follow-link-selected-style)))
  34. (xwidget-plus-js-def follow-link cleanup ()
  35. "Remove all custom class from links.""
  36. document.querySelectorAll('a').forEach(a => {
  37. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  38. });
  39. ")
  40. (xwidget-plus-js-def follow-link highlight (ids selected)
  41. "Highlight IDS as candidate and SELECTED as selected.""
  42. document.querySelectorAll('a').forEach((a, id) => {
  43. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  44. if (selected == id) {
  45. a.classList.add('xwidget-plus-follow-link-selected');
  46. a.scrollIntoView({behavior: 'smooth', block: 'center'});
  47. } else if (ids.includes(id)) {
  48. a.classList.add('xwidget-plus-follow-link-candidate');
  49. }
  50. });
  51. ")
  52. (xwidget-plus-js-def follow-link action (link-id)
  53. "Click on the link identified by LINK-ID""
  54. __xwidget_plus_follow_link_cleanup();
  55. document.querySelectorAll('a')[link_id].click();
  56. ")
  57. (xwidget-plus-js-def follow-link fetch-links ()
  58. "Fetch all visible, non empty links from the current page.""
  59. var r = {};
  60. document.querySelectorAll('a').forEach((a, i) => {
  61. if (a.offsetWidth || a.offsetHeight || a.getClientRects().length) {
  62. if (a.innerText.match(/\\\\S/))
  63. r[i] = a.innerText;
  64. }
  65. });
  66. return r;
  67. ")
  68. ;; Completion backend class
  69. (defclass xwidget-plus-completion-backend () ((collection) (text)))
  70. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend))
  71. "Return the BACKEND selected link and the candidates.
  72. The return value is a list whose first element is the selected id
  73. link and the rest are the candidates ids.
  74. Return nil if the backend does not support narrowing selection list.")
  75. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend) 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. (defclass xwidget-plus-completion-backend-ido (xwidget-plus-completion-backend) ())
  110. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ido))
  111. (let ((collection (oref backend collection)))
  112. (when collection
  113. (seq-map (lambda (i) (cdr (assoc i collection))) ido-matches))))
  114. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ido) prompt collection action update-fn)
  115. (let ((choices (seq-map #'car collection)))
  116. (advice-add #'ido-set-matches :after update-fn)
  117. (let ((link (unwind-protect
  118. (cdr (assoc (ido-completing-read prompt choices nil t) collection))
  119. (oset backend collection nil)
  120. (advice-remove #'ido-set-matches #'update-fn))))
  121. (funcall action link)))))
  122. ;; Ivy backend using completing read
  123. (with-eval-after-load 'ivy
  124. (defclass xwidget-plus-completion-backend-ivy (xwidget-plus-completion-backend) ())
  125. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ivy))
  126. (with-current-buffer (ivy-state-buffer ivy-last)
  127. (let* ((collection (ivy-state-collection ivy-last))
  128. (current (ivy-state-current ivy-last))
  129. (candidates (ivy--filter ivy-text ivy--all-candidates))
  130. (result (cons current candidates)))
  131. (seq-map (lambda (c) (cdr (nth (get-text-property 0 'idx c) collection))) result))))
  132. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ivy) prompt collection action update-fn)
  133. (ivy-read "Link: " collection :require-match t :action (lambda (v) (funcall action (cdr v))) :update-fn update-fn)))
  134. ;; Helm backend
  135. (with-eval-after-load 'helm
  136. (defclass xwidget-plus-completion-backend-helm (xwidget-plus-completion-backend) ((candidates)))
  137. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-helm))
  138. (let* ((candidates (oref backend candidates))
  139. (selection (helm-get-selection))
  140. (selected (when selection (cdr (elt (oref backend collection) selection))))
  141. (result (seq-map #'cdr candidates)))
  142. (cons selected result)))
  143. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-helm) prompt collection action update-fn)
  144. (add-hook 'helm-after-initialize-hook (lambda ()
  145. (with-current-buffer "*helm-xwidget-plus*"
  146. (add-hook 'helm-move-selection-after-hook update-fn nil t)))
  147. nil t)
  148. (helm :sources
  149. (helm-make-source "Xwidget Plus" 'helm-source-sync
  150. :candidates collection
  151. :action action
  152. :filtered-candidate-transformer (lambda (candidates _)
  153. (oset backend candidates candidates)
  154. (funcall update-fn)
  155. candidates))
  156. :prompt prompt
  157. :buffer "*helm-xwidget-plus*")))
  158. (defun xwidget-plus-follow-link-make-backend ()
  159. "Instanciate a completion backend."
  160. (cond ((eq xwidget-plus-completion-system 'default)
  161. (cond ((featurep 'ivy)
  162. #'xwidget-plus-completion-backend-ivy)
  163. ((featurep 'helm)
  164. #'xwidget-plus-completion-backend-helm)
  165. ((featurep 'ido)
  166. #'xwidget-plus-completion-backend-ido)
  167. (t #'xwidget-plus-completion-backend-default)))
  168. ((eq xwidget-plus-completion-system 'ivy)
  169. #'xwidget-plus-completion-backend-ivy)
  170. ((eq xwidget-plus-completion-system 'helm)
  171. #'xwidget-plus-completion-backend-helm)
  172. ((eq xwidget-plus-completion-system 'ido)
  173. #'xwidget-plus-completion-backend-ido)
  174. ((eq xwidget-plus-completion-system 'default)
  175. #'xwidget-plus-completion-backend-default)
  176. (t xwidget-plus-completion-system)))
  177. (defvar xwidget-plus-follow-link-completion-backend-instance '())
  178. (defun xwidget-plus-follow-link-update (xwidget)
  179. "Highligh LINKS in XWIDGET buffer when updating candidates."
  180. (let ((links (xwidget-plus-follow-link-candidates xwidget-plus-follow-link-completion-backend-instance)))
  181. (when links
  182. (let* ((selected (car links))
  183. (candidates (cdr links)))
  184. (xwidget-plus-follow-link-highlight xwidget candidates selected)))))
  185. (defun xwidget-plus-follow-link-trigger-action (xwidget selected)
  186. "Activate link matching SELECTED in XWIDGET LINKS."
  187. (xwidget-plus-follow-link-action xwidget selected))
  188. (defun xwidget-plus-follow-link-format-link (str)
  189. "Format link title STR."
  190. (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
  191. (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
  192. (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
  193. (replace-regexp-in-string "[[:space:]]+" " " str))
  194. (defun xwidget-plus-follow-link-prepare-links (links)
  195. "Prepare the alist of LINKS."
  196. (seq-sort-by (lambda (v) (cdr v)) #'<
  197. (seq-map (lambda (v) (cons (xwidget-plus-follow-link-format-link (cdr v)) (string-to-number (car v))))
  198. links)))
  199. (defun xwidget-plus-follow-link-callback (links)
  200. "Ask for a link belonging to the alist LINKS."
  201. (let* ((xwidget (xwidget-webkit-current-session))
  202. (links (xwidget-plus-follow-link-prepare-links links))
  203. link)
  204. (oset xwidget-plus-follow-link-completion-backend-instance collection links)
  205. (unwind-protect
  206. (condition-case nil
  207. (xwidget-plus-follow-link-read xwidget-plus-follow-link-completion-backend-instance
  208. "Link: " links
  209. (apply-partially #'xwidget-plus-follow-link-trigger-action xwidget)
  210. (apply-partially #'xwidget-plus-follow-link-update xwidget))
  211. (quit (xwidget-plus-follow-link-cleanupxwidget))))
  212. (oset xwidget-plus-follow-link-completion-backend-instance collection nil)))
  213. ;;;###autoload
  214. (defun xwidget-plus-follow-link (&optional xwidget)
  215. "Ask for a link in the XWIDGET session or the current one and follow it."
  216. (interactive)
  217. (setq xwidget-plus-follow-link-completion-backend-instance (funcall (xwidget-plus-follow-link-make-backend)))
  218. (let ((xwidget (or xwidget (xwidget-webkit-current-session))))
  219. (xwidget-plus-inject-style xwidget "__xwidget_plus_follow_link_style" (xwidget-plus-follow-link-style-definition))
  220. (xwidget-plus-js-inject xwidget 'follow-link)
  221. (xwidget-plus-follow-link-fetch-links xwidget #'xwidget-plus-follow-link-callback)))
  222. ;; Local Variables:
  223. ;; eval: (mmm-mode)
  224. ;; eval: (mmm-add-group 'elisp-js '((elisp-rawjs :submode js-mode
  225. ;; :face mmm-code-submode-face
  226. ;; :delimiter-mode nil
  227. ;; :front "--js \"" :back "\" js--")
  228. ;; (elisp-defjs :submode js-mode
  229. ;; :face mmm-code-submode-face
  230. ;; :delimiter-mode nil
  231. ;; :front "xwidget-plus-js-def .*\n.*\"\"\n" :back "\")\n")))
  232. ;; mmm-classes: elisp-js
  233. ;; End:
  234. (provide 'xwidget-plus-follow-link)
  235. ;;; xwidget-plus-follow-link.el ends here