diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-12 11:47:26 -0500 |
| commit | 092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch) | |
| tree | ea81999b8442246c978b364dd90e8c752af50db5 /modules/org-webclipper.el | |
changing repositories
Diffstat (limited to 'modules/org-webclipper.el')
| -rw-r--r-- | modules/org-webclipper.el | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/modules/org-webclipper.el b/modules/org-webclipper.el new file mode 100644 index 00000000..c7b80499 --- /dev/null +++ b/modules/org-webclipper.el @@ -0,0 +1,145 @@ +;;; org-webclipper.el --- Web Page Clipping Workflow to Org Roam -*- coding: utf-8; lexical-binding: t; -*- + +;;; Commentary: +;; +;; Allows saving a copy of the page EWW is visiting for offline reading. +;; In other words, it's a "Pocket/Instapaper" that collects the articles in an Emacs org-mode file. +;; +;; I review the articles, then add the ones I want for future reference by moving it to an +;; org-roam file. +;; +;;; Code: + +(require 'user-constants) ;; for location of 'webclipped-file' + +;; ---------------------------- Org Webpage Clipper ---------------------------- + +(defun cj/org-webpage-clipper () + "Capture the current web page for later viewing in an Org file. + +Return the yanked content as a string so templates can insert it." + (interactive) + (let* ((source-buffer (org-capture-get :original-buffer)) + (source-mode (with-current-buffer source-buffer major-mode))) + (cond + ((eq source-mode 'w3m-mode) + (with-current-buffer source-buffer + (org-w3m-copy-for-org-mode))) + ((eq source-mode 'eww-mode) + (with-current-buffer source-buffer + (org-eww-copy-for-org-mode))) + (t + (error "Not valid -- must be in w3m or eww mode"))) + ;; extract the webpage content from the kill ring + (car kill-ring))) + +;; ------------------------------ Capture Template ----------------------------- + +(with-eval-after-load 'org-capture + ;; Ensure org-capture-templates exists before adding to it + (unless (boundp 'org-capture-templates) + (setq org-capture-templates nil)) + + ;; Add the webclipper template to org-capture-templates + (add-to-list 'org-capture-templates + '("w" "Web Page Clipper" entry + (file+headline webclipped-file "Webclipped Inbox") + "* %a\nURL: %L\nCaptured On:%U\n%(cj/org-webpage-clipper)\n" + :prepend t :immediate-finish t) + t)) + +;; ------------------------ Org-Branch To Org-Roam-Node ------------------------ + +(defun cj/org-link-get-description (text) + "Extract the description from an org link, or return the text unchanged. +If TEXT contains an org link like [[url][description]], return description. +If TEXT contains multiple links, only process the first one. +Otherwise return TEXT unchanged." + (if (string-match "\\[\\[\\([^]]+\\)\\]\\(?:\\[\\([^]]+\\)\\]\\)?\\]" text) + (let ((description (match-string 2 text)) + (url (match-string 1 text))) + ;; If there's a description, use it; otherwise use the URL + (or description url)) + text)) + +(defun cj/move-org-branch-to-roam () + "Move the org subtree at point to a new org-roam node. +The node filename will be timestamp-based with the heading name. +The heading becomes the node title, and the entire subtree is demoted to level 1. +If the heading contains a link, extract the description for the title." + (interactive) + (unless (org-at-heading-p) + (user-error "Not at an org heading")) + + (let* ((heading-components (org-heading-components)) + (current-level (nth 0 heading-components)) + (raw-title (nth 4 heading-components)) + ;; Extract clean title from potential link + (title (cj/org-link-get-description raw-title)) + (timestamp (format-time-string "%Y%m%d%H%M%S")) + ;; Convert title to filename-safe format + (title-slug (replace-regexp-in-string + "[^a-zA-Z0-9]+" "-" + (downcase title))) + ;; Remove leading/trailing hyphens + (title-slug (replace-regexp-in-string + "^-\\|-$" "" title-slug)) + (filename (format "%s-%s.org" timestamp title-slug)) + (filepath (expand-file-name filename org-roam-directory)) + ;; Generate a unique ID for the node + (node-id (org-id-new)) + ;; Store the subtree in a temporary buffer + subtree-content) + + ;; Copy the subtree content + (org-copy-subtree) + (setq subtree-content (current-kill 0)) + + ;; Now cut it to remove from original buffer + (org-cut-subtree) + + ;; Process the subtree to demote it to level 1 + (with-temp-buffer + (org-mode) + (insert subtree-content) + ;; Demote the entire tree so the top level becomes level 1 + (goto-char (point-min)) + (when (> current-level 1) + (let ((demote-count (- current-level 1))) + (while (re-search-forward "^\\*+ " nil t) + (beginning-of-line) + (dotimes (_ demote-count) + (when (looking-at "^\\*\\*") + (delete-char 1))) + (forward-line)))) + (setq subtree-content (buffer-string))) + + ;; Create the new org-roam file + (with-temp-file filepath + ;; Insert the org-roam template with ID at file level + (insert ":PROPERTIES:\n") + (insert ":ID: " node-id "\n") + (insert ":END:\n") + (insert "#+TITLE: " title "\n") + (insert "#+CATEGORY: " title "\n") + (insert "#+FILETAGS: Topic\n\n") + + ;; Insert the demoted subtree content + (insert subtree-content)) + + ;; Sync the org-roam database + (org-roam-db-sync) + + ;; Message to user + (message "'%s' added as an org-roam node." title))) + +;; ----------------------------- Webclipper Keymap ----------------------------- + +;; Buffer & file operations prefix and keymap +(define-prefix-command 'cj/webclipper-map nil + "Keymap for weblipper operations.") +(define-key cj/custom-keymap "w" 'cj/webclipper-map) +(define-key cj/webclipper-map "N" 'cj/move-org-branch-to-roam) ;; for node + +(provide 'org-webclipper) +;;; org-webclipper.el ends here. |
