xwidget-plus-follow-link.el 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. (defconst xwidget-plus-follow-link-script (--js "
  35. function __xwidget_plus_follow_link_cleanup() {
  36. document.querySelectorAll('a').forEach(a => {
  37. a.classList.remove('xwidget-plus-follow-link-candidate', 'xwidget-plus-follow-link-selected');
  38. });
  39. }
  40. function __xwidget_plus_follow_link_highlight(json, selected) {
  41. var ids = JSON.parse(json);
  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. function __xwidget_plus_follow_link_action(id) {
  53. __xwidget_plus_follow_link_cleanup();
  54. document.querySelectorAll('a')[id].click();
  55. }
  56. function __xwidget_plus_follow_link_links() {
  57. var r = {};
  58. document.querySelectorAll('a').forEach((a, i) => {
  59. if (a.offsetWidth || a.offsetHeight || a.getClientRects().length) {
  60. if (a.innerText.match(/\\\\S/))
  61. r[i] = a.innerText;
  62. }
  63. });
  64. return r;
  65. }
  66. " js--))
  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) prompt collection action update-fn)
  75. "use BACKEND to PROMPT the user for a link in COLLECTION.
  76. ACTION should be called with the resulting link.
  77. UPDATE-FN is a function that can be called when the candidates
  78. list is narrowed. It will highlight the link list in the
  79. browser.")
  80. ;; Default backend using completing-read
  81. (defclass xwidget-plus-completion-backend-default (xwidget-plus-completion-backend) ())
  82. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-default))
  83. "Return the BACKEND selected link and the candidates.
  84. The return value is a list whose first element is the selected id
  85. link and the rest are the candidates ids.
  86. Return nil if the backend does not support narrowing selection list."
  87. (let* ((collection (oref backend collection))
  88. (text (oref backend text))
  89. (matches (seq-filter (lambda (i) (string-match-p (concat "^" (regexp-quote text)) (car i))) collection))
  90. (matches (seq-map #'cdr matches)))
  91. (if (= 1 (length matches))
  92. matches
  93. (cons nil matches))))
  94. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-default) prompt collection action update-fn)
  95. "use BACKEND to PROMPT the user for a link in COLLECTION.
  96. ACTION should be called with the resulting link.
  97. UPDATE-FN is a function that can be called when the candidates
  98. list is narrowed. It will highlight the link list in the
  99. browser."
  100. (funcall action (cdr (assoc (completing-read prompt (lambda (str pred _)
  101. (oset backend text str)
  102. (funcall update-fn)
  103. (try-completion str collection pred))
  104. nil t)
  105. collection))))
  106. ;; Ido backend using ido-completing-read
  107. (defclass xwidget-plus-completion-backend-ido (xwidget-plus-completion-backend) ())
  108. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ido))
  109. (let ((collection (oref backend collection)))
  110. (when collection
  111. (seq-map (lambda (i) (cdr (assoc i collection))) ido-matches))))
  112. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ido) prompt collection action update-fn)
  113. (let ((choices (seq-map #'car collection)))
  114. (advice-add #'ido-set-matches :after update-fn)
  115. (let ((link (cdr (assoc (ido-completing-read prompt choices nil t) collection))))
  116. (oset backend collection nil)
  117. (advice-remove #'ido-set-matches #'update-fn)
  118. (funcall action link))))
  119. ;; Ivy backend using completing read
  120. (defclass xwidget-plus-completion-backend-ivy (xwidget-plus-completion-backend) ())
  121. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-ivy))
  122. (with-current-buffer (ivy-state-buffer ivy-last)
  123. (let* ((collection (ivy-state-collection ivy-last))
  124. (current (ivy-state-current ivy-last))
  125. (candidates (ivy--filter ivy-text ivy--all-candidates))
  126. (result (cons current candidates)))
  127. (seq-map (lambda (c) (cdr (nth (get-text-property 0 'idx c) collection))) result))))
  128. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-ivy) prompt collection action update-fn)
  129. (ivy-read "Link: " collection :require-match t :action (lambda (v) (funcall action (cdr v))) :update-fn update-fn))
  130. ;; Helm backend
  131. (defclass xwidget-plus-completion-backend-helm (xwidget-plus-completion-backend) ((candidates)))
  132. (cl-defmethod xwidget-plus-follow-link-candidates ((backend xwidget-plus-completion-backend-helm))
  133. (let* ((candidates (oref backend candidates))
  134. (selection (helm-get-selection))
  135. (selected (when selection (cdr (elt (oref backend collection) selection))))
  136. (result (seq-map #'cdr candidates)))
  137. (cons selected result)))
  138. (cl-defmethod xwidget-plus-follow-link-read ((backend xwidget-plus-completion-backend-helm) prompt collection action update-fn)
  139. (add-hook 'helm-after-initialize-hook (lambda ()
  140. (with-current-buffer "*helm-xwidget-plus*"
  141. (add-hook 'helm-move-selection-after-hook update-fn nil t)))
  142. nil t)
  143. (helm :sources
  144. (helm-make-source "Xwidget Plus" 'helm-source-sync
  145. :candidates collection
  146. :action action
  147. :filtered-candidate-transformer (lambda (candidates _)
  148. (oset backend candidates candidates)
  149. (funcall update-fn)
  150. candidates))
  151. :prompt prompt
  152. :buffer "*helm-xwidget-plus*"))
  153. (defvar xwidget-plus-completion-backend-instance (xwidget-plus-completion-backend))
  154. (defun xwidget-plus-follow-link-highlight (xwidget)
  155. "Highligh LINKS in XWIDGET buffer when updating candidates."
  156. (let ((links (xwidget-plus-follow-link-candidates xwidget-plus-completion-backend-instance)))
  157. (when links
  158. (let* ((selected (car links))
  159. (candidates (cdr links))
  160. (script (--js "__xwidget_plus_follow_link_highlight('%s', %s);" js-- (json-serialize (vconcat candidates)) (or selected "null"))))
  161. (xwidget-webkit-execute-script xwidget script)))))
  162. (defun xwidget-plus-follow-link-exit (xwidget)
  163. "Exit follow link mode in XWIDGET."
  164. (let ((script "__xwidget_plus_follow_link_cleanup();"))
  165. (xwidget-webkit-execute-script xwidget script)))
  166. (defun xwidget-plus-follow-link-action (xwidget selected)
  167. "Activate link matching SELECTED in XWIDGET LINKS."
  168. (let ((script (--js "__xwidget_plus_follow_link_action(%s);" js-- selected)))
  169. (xwidget-webkit-execute-script xwidget script)))
  170. (defun xwidget-plus-follow-link-format-link (str)
  171. "Format link title STR."
  172. (setq str (replace-regexp-in-string "^[[:space:][:cntrl:]]+" "" str))
  173. (setq str (replace-regexp-in-string "[[:space:][:cntrl:]]+$" "" str))
  174. (setq str (replace-regexp-in-string "[[:cntrl:]]+" "/" str))
  175. (replace-regexp-in-string "[[:space:]]+" " " str))
  176. (defun xwidget-plus-follow-link-prepare-links (links)
  177. "Prepare the alist of LINKS."
  178. (seq-sort-by (lambda (v) (cdr v)) #'<
  179. (seq-map (lambda (v) (cons (xwidget-plus-follow-link-format-link (cdr v)) (string-to-number (car v))))
  180. links)))
  181. (defun xwidget-plus-follow-link-callback (links)
  182. "Ask for a link belonging to the alist LINKS."
  183. (let* ((xwidget (xwidget-webkit-current-session))
  184. (links (xwidget-plus-follow-link-prepare-links links))
  185. link)
  186. (oset xwidget-plus-completion-backend-instance collection links)
  187. (unwind-protect
  188. (condition-case nil
  189. (xwidget-plus-follow-link-read xwidget-plus-completion-backend-instance
  190. "Link: " links
  191. (apply-partially #'xwidget-plus-follow-link-action xwidget)
  192. (apply-partially #'xwidget-plus-follow-link-highlight xwidget))
  193. (quit (xwidget-plus-follow-link-exit xwidget))))
  194. (oset xwidget-plus-completion-backend-instance collection nil)))
  195. ;;;###autoload
  196. (defun xwidget-plus-follow-link (&optional xwidget)
  197. "Ask for a link in the XWIDGET session or the current one and follow it."
  198. (interactive)
  199. (let ((xwidget (or xwidget (xwidget-webkit-current-session)))
  200. (script (--js "__xwidget_plus_follow_link_links();" js--)))
  201. (xwidget-plus-inject-style xwidget "__xwidget_plus_follow_link_style" (xwidget-plus-follow-link-style-definition))
  202. (xwidget-plus-inject-script xwidget "__xwidget_plus_follow_link_script" xwidget-plus-follow-link-script)
  203. (xwidget-webkit-execute-script xwidget script #'xwidget-plus-follow-link-callback)))
  204. ;; Local Variables:
  205. ;; eval: (mmm-mode)
  206. ;; eval: (mmm-add-classes '((elisp-js :submode js-mode :face mmm-code-submode-face :delimiter-mode nil :front "--js \"" :back "\" js--")))
  207. ;; mmm-classes: elisp-js
  208. ;; End:
  209. (provide 'xwidget-plus-follow-link)
  210. ;;; xwidget-plus-follow-link.el ends here