blob: c7b80499818ffa9f9c8e3e32eecad03016fe1e6b (
plain)
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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.
 |