-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathmd-roam.el
936 lines (815 loc) · 38.4 KB
/
md-roam.el
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
;;; md-roam.el --- description -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2020-2024 Noboru Ota
;;
;; Author: Noboru Ota <https://github.com/nobiot>
;; URL: https://github.com/nobiot/md-roam
;; Version: 2.0.1
;; Last modified: 2024-01-17T234302
;; Package-Requires: ((emacs "27.1") (org-roam "2.1.0") (markdown-mode "2.5"))
;; Keywords: markdown, zettelkasten, note-taking, writing, org, org-roam
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Use org-roam with markdown files by adding md-roam to it. md-roam extends
;; the features and functions provided by org-roam to support markdown files
;; in addition to org
;; Refer to README in the GitHub repo for instruction, configuraiton,
;; and features supported in more detail
;;; Code:
;;;; Requirements
(eval-when-compile (require 'subr-x))
;; Markdown
(require 'markdown-mode)
;; Org
(require 'org)
(require 'ol)
(require 'org-macs)
;; Org-roam and its prequisites directly used by Md-roam
(require 'org-roam)
(require 'org-roam-db)
(require 'org-roam-utils)
(require 'emacsql)
;;;; Customization
(defgroup md-roam nil
"Use markdown files in Org-roam."
:group 'org-roam
:prefix "md-roam-"
:link '(url-link :tag "Github" "https://github.com/nobiot/md-roam"))
(defcustom md-roam-file-extension "md"
"Define the extesion to be used for Md-roam within Org-roam directory.
Unlike 'org-roam-file-extension', this is a single value, not a list.
It is intended to be used for you to define a different markdown extension,
such as .md and .markdown."
:group 'md-roam
:type '(choice
(string)
(repeat :tag "List of extensions (string)" string)))
(defcustom md-roam-node-insert-type 'title-or-alias
"Define whether ID or title/aliase should be inserted.
This is for `md-roam-node-insert'. If 'title-and-alias, the
resultant wiki link will be \"[[title]]\. If 'ID, it will be
\"[[ID]] title\"."
:type '(choice (const :tag "Title or alias" title-or-alias)
(const :tag "Node ID" id))
:group 'md-roam)
;;;; Variables
(defvar md-roam-db-compatible-version 18
"`Compatible 'org-roam-db-version'.
This is as described in \(info \"(org-roam\)Developer's Guide to Org-roam\"\).")
;;;; These regular expressions are modified version of
;;;; `markdown-regex-yaml-metadata-border.
;;;; I am adding "^" to indicate that the a line needs to
;;;; start with the delineator.
(defvar md-roam-regex-yaml-font-matter-beginning
"\\(^-\\{3\\}\\)$"
"Regexp for the beginning of YAML front matter section.
In `markdown-mode', beginning and ending are the same: \"---\".")
(defvar md-roam-regex-yaml-font-matter-ending
"\\(^-\\{3\\}\\)$"
"Regexp for the ending of YAML front matter section.
Separate regular expressions for beginning and ending are defined
here because in some markdown conventions, the ending is
delineated by \"```\".
This can be potentially supported setting a custom regexp for
the ending delineator.
If you change this to `\\(^`'\\{3\\}\\)$', you should be able to
support YAML matter ending with ```. I am not testing it, though.")
(defvar md-roam-regex-title
"\\(^title:[ \t]*\\)\\(.*\\)")
(defvar md-roam-regex-id
"\\(^id:[ \t]*\\)\\(.*\\)")
(defvar md-roam-regex-aliases
;; Assumed to be case insensitive
"\\(^.*ROAM_ALIASES:[ \t]*\\)\\(.*\\)")
(defvar md-roam-regex-ref-keys
;; Assumed to be case insensitive
"\\(^.*ROAM_REFS:[ \t]*\\)\\(.*\\)")
(defvar md-roam-regex-headline
(concat "^\s*\n" ;exludes YAML front matter
"\\(.*$\\)\n\\(^[=-]+$\\)" ;heading with '=' and '-'
"\\|" ;regex 'or'
"\\(^#+ \\)\\(.*$\\)")) ;heading with '#'
(defvar md-roam-regex-in-text-citation-2
"\\(?:[^[:alnum:]]\\|^\\)\\(-?@\\)\\([-a-zA-Z0-9_+:]+\\)"
"Regular expression for stand-alone citation with no anchor.
Regexp for pandoc style citation for link extraction
Copy from pandco-mode to remove dependency
[@bibkey], [@bibkey1, xx; bibkey2, yy]
https://pandoc.org/MANUAL.html#citations
Blah blah [see @doe99, pp. 33-35; also @smith04, chap. 1].
Blah blah [@doe99, pp. 33-35, 38-39 and *passim*].
Blah blah [@smith04; @doe99].
[@smith{ii, A, D-Z}, with a suffix]
[@smith, {pp. iv, vi-xi, (xv)-(xvii)} with suffix here]
A minus sign (-) before the @ will suppress mention
Smith says blah [-@smith04].
in-text citation:
@smith04 says blah.
@smith04 [p. 33] says blah.
With my testing, citation-2 can detect all the cases for md-roam")
(defvar md-roam-regex-tags-zettlr-style
"\\([^/s]\\)\\([#@][[:alnum:]_-]+\\)"
"Regexp for extracting tags.
This regexp is intended to be compatible with Zettlr:
`#tag1 #tag-with-hyphen #tag_with_underscore'
Note that iA Writer treats hyphen (-) as a word delimiter
within a tag. That is, iA Writer treats #tag-hyphen tagged as
#tag, and ignores `-hyphen'.
If iA Writer's stricter style is preferred, the regexp should
be defined as:
`\\([^/s]\\)\\([#@][[:alnum:]_]+\\)'")
(defconst md-roam-regex-link-inline
"\\(?1:!\\)?\\(?2:\\[\\)\\(?3:\\^?\\(?:\\\\\\]\\|[^]]\\)*\\|\\)\\(?4:\\]\\)\\(?5:(\\)\\(?6:[^)]*?\\)\\(?:\\s-+\\(?7:\"[^\"]*\"\\)\\)?\\(?8:)\\)"
"Copy of `markdown-regex-link-inline' from Markdown Mode.
Regexp for inline links [description](link) and images ![description](link).
Regular expression for a [text](file) or an image link ![text](file).
Group 1 matches the leading exclamation point (optional).
Group 2 matches the opening square bracket.
Group 3 matches the text inside the square brackets.
Group 4 matches the closing square bracket.
Group 5 matches the opening parenthesis.
Group 6 matches the URL.
Group 7 matches the title (optional).
Group 8 matches the closing parenthesis.")
;;;; Commands
;;;###autoload
(define-minor-mode md-roam-mode
"Md-roam mode needs to be turned before `org-roam-db-sync'.
It needs to be turned on before `org-roam-db-autosync-mode'."
:init-value nil
:lighter "md-roam"
:global t
(cond
(md-roam-mode
;; Check org-roam-db-version
;; This is as described in (info "(org-roam)Developer's Guide to Org-roam")
(if (not (eq org-roam-db-version md-roam-db-compatible-version))
(progn
(message (format "Md-roam not turned on; `org-roam-db-version' %d is not compatible" org-roam-db-version))
(setq md-roam-mode nil))
;; Org-roam cache
(advice-add #'org-roam-db-update-file :before-until #'md-roam-db-update-file)
;; Other interactive commands
(advice-add #'org-roam-node-insert :before-until #'md-roam-node-insert)
(advice-add #'markdown-follow-wiki-link :before-until #'md-roam-follow-wiki-link)
;; This avoids capture process to add ID in the Org property drawer
(add-hook 'org-roam-capture-preface-hook #'md-roam-capture-preface)
(advice-add #'org-id-get :before-until #'md-roam-id-get)
(advice-add #'org-roam-id-at-point :before-until #'md-roam-id-at-point)
;; `org-roam-mode' buffer
(advice-add #'org-roam-node-at-point
:before-until #'md-roam-node-at-point)
(advice-add #'org-roam-preview-get-contents
:before-until #'md-roam-preview-get-contents)
;; For `before-save-hook'
(advice-add #'org-roam--replace-roam-links-on-save-h
:before-until #'md-roam--replace-roam-links-on-save-h)
;; For `org-roam-buffer-p'
(advice-add #'org-roam-buffer-p :before-until #'md-roam-buffer-p)
;; Avoid invalid-regexp in `org-roam-node-open'
(advice-add #'org-show-context :before-until #'md-roam-do-not-show-context)
;; Insert wiki-link when inserting a new note with using `org-roam-insert'
(advice-add #'org-roam-capture--finalize-insert-link
:before-until #'md-roam-capture--finalize-insert-link)
;; Completion-at-point
;; Append to the back of the functions list so that md-roam's one get called
;; before org-roam ones (org-roam dolist, resulting in reversing the order)
(add-to-list 'org-roam-completion-functions
#'md-roam-complete-wiki-link-at-point 'append)
(add-to-list 'org-roam-completion-functions
#'md-roam-complete-everywhere 'append)))
(t
;; Deactivate
(advice-remove #'org-roam-db-update-file #'md-roam-db-update-file)
(advice-remove #'org-roam-node-insert #'md-roam-node-insert)
(advice-remove #'markdown-follow-wiki-link #'md-roam-follow-wiki-link)
(remove-hook 'org-roam-capture-preface-hook #'md-roam-capture-preface)
(advice-remove #'org-id-get #'md-roam-id-get)
(advice-remove #'org-roam-id-at-point #'md-roam-id-at-point)
(advice-remove #'org-roam-node-at-point #'md-roam-node-at-point)
(advice-remove #'org-roam-preview-get-contents
#'md-roam-preview-get-contents)
(advice-remove #'org-roam--replace-roam-links-on-save-h
#'md-roam--replace-roam-links-on-save-h)
(advice-remove #'org-roam-buffer-p #'md-roam-buffer-p)
(advice-remove #'org-show-context #'md-roam-do-not-show-context)
(advice-remove #'org-roam-capture--finalize-insert-link
#'md-roam-capture--finalize-insert-link)
(remove-hook 'org-roam-completion-functions
#'md-roam-complete-wiki-link-at-point)
(remove-hook 'org-roam-completion-functions
#'md-roam-complete-everywhere))))
;;;; Functions
;;; File utilities
;;; Macros seem to need to be defined before functions that use them
(defmacro md-roam-with-file (file keep-buf-p &rest body)
"Execute BODY within FILE for Md-roam.
If FILE is nil, execute BODY in the current buffer.
Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited."
(declare (indent 2) (debug t))
`(let* (new-buf
(auto-mode-alist nil)
(find-file-hook nil)
(buf (or (and (not ,file)
(current-buffer)) ;If FILE is nil, use current buffer
(find-buffer-visiting ,file) ; If FILE is already visited, find buffer
(progn
(setq new-buf t)
(find-file-noselect ,file)))) ; Else, visit FILE and return buffer
res)
(with-current-buffer buf
(unless (equal major-mode 'markdown-mode)
;; Don't delay mode hooks
;; not done again.
(markdown-mode))
(setq res (progn ,@body))
(unless (and new-buf (not ,keep-buf-p))
(save-buffer)))
(if (and new-buf (not ,keep-buf-p))
(when (find-buffer-visiting ,file)
(kill-buffer (find-buffer-visiting ,file))))
res))
;;;;; Private
;; None of the functions in Md-roam are meant to be interactive commands.
;; This is because they are meant to extend Org-roam functions (mostly in
;; `org-roam-db') by means of advice. This means that you as a user don't
;; have to thinkg about the difference between Org-roam and Md-roam. You can
;; just use the same Org-roam commands such as `org-roam-node-insert' and
;; `org-roam-node-find' within markdown files. The switch of context is
;; detected by the file extension -- e.g. .md vs .org -- and Md-roam will
;; take care of the difference of the context.
;;------------------------------------------------------------------------------
;;;;; DB related functions for `org-roam-db'
(defun md-roam-db-update-file (&optional file-path _no-require)
"Update Org-roam cache for FILE-PATH.
This function is meant to be used as advising function for
`org-roam-db-update-file.'"
(when (md-roam--markdown-file-p (or file-path (buffer-file-name (buffer-base-buffer))))
(md-roam-with-file file-path nil
(setq file-path (or file-path (buffer-file-name (buffer-base-buffer))))
(let ((content-hash (org-roam-db--file-hash file-path))
(db-hash (caar (org-roam-db-query [:select hash :from files
:where (= file $s1)] file-path))))
(unless (string= content-hash db-hash)
(md-roam-db-do-update))
;; For before-until advice
t))))
(defun md-roam-db-do-update ()
"Update db cache without checking if the file has been changed.
Requied for wiki link capture."
(emacsql-with-transaction (org-roam-db)
(save-excursion
(org-roam-db-clear-file)
(md-roam-db-insert-file)
(md-roam-db-insert-file-node)
(md-roam-db-insert-wiki-links)
(md-roam-db-insert-citations)
(md-roam-db-insert-links))))
(defun md-roam-db-insert-file (&optional hash)
"Update the files table for the current buffer.
If UPDATE-P is non-nil, first remove the file in the database.
If HASH is non-nil, use that as the file's hash without recalculating it."
(let* ((file (buffer-file-name))
(file-title (or (md-roam-get-title) (file-name-base file)))
(attr (file-attributes file))
(atime (file-attribute-access-time attr))
(mtime (file-attribute-modification-time attr))
(hash (or hash (org-roam-db--file-hash file))))
(org-roam-db-query
[:insert :into files
:values $v1]
(list (vector file file-title hash atime mtime)))))
(defun md-roam-db-insert-file-node ()
"Insert the file-level node into the Org-roam cache."
;; `org-roam-db-update-file' turns the mode to org-mode (in `org-roam-with-file' macro)
;; `markdown-mode' needs to be explicitly turned back on.
;; (markdown-mode)
;; Run org-roam hooks to re-set after-save-hooks, etc.
(run-hooks 'org-roam-find-file-hook)
;; `org-with-point-at' macro does not seem to assume the buffer is Org
(org-with-point-at 1
(when-let ((id (md-roam-get-id)))
(let* ((file (buffer-file-name (buffer-base-buffer)))
(title (or (md-roam-get-title)
(file-relative-name file org-roam-directory)))
(pos (point))
(todo nil)
(priority nil)
(scheduled nil)
(deadline nil)
(level 0)
(tags (md-roam-get-tags))
;; Properties are required for `org-roam-ui'
;; TODO other properties in frontmatter?
(properties (list (cons "TITLE" title) (cons "ID" id)))
(olp nil))
(org-roam-db-query!
(lambda (err)
(lwarn 'org-roam :warning "%s for %s (%s) in %s"
(error-message-string err)
title id file))
[:insert :into nodes
:values $v1]
(vector id file level pos todo priority
scheduled deadline title properties olp))
(when tags
(org-roam-db-query
[:insert :into tags
:values $v1]
(mapcar (lambda (tag)
(vector id (substring-no-properties tag)))
tags)))
(md-roam-db-insert-aliases)
(md-roam-db-insert-refs)))))
(defun md-roam-db-insert-wiki-links ()
"Insert Markdown wiki links in current buffer into Org-roam cache."
(org-with-point-at (md-roam-get-yaml-front-matter-endpoint)
(let ((type "id")
(source (md-roam-get-id))
(properties (list :outline nil)))
(when source
(while (re-search-forward "\\[\\[\\([^]]+\\)\\]\\]" nil t)
(let* ((name (match-string-no-properties 1))
(node (or (org-roam-node-from-title-or-alias name)
(org-roam-node-from-title-or-alias (replace-regexp-in-string "[\n ]+" " " name))
(org-roam-node-create :id name)))
(path (org-roam-node-id node)))
;; insert to cache the link only there is a file for the
;; destination node
(when (org-roam-node-file (org-roam-populate node))
(org-roam-db-query
[:insert :into links
:values $v1]
(vector (point) source path type properties)))))))))
(defun md-roam-db-insert-links ()
"Insert URL and file links in current buffer into Org-roam cache.
URLs are for refs. File links are for backlinks if the target
files are in `org-roam-directory'."
(org-with-point-at (md-roam-get-yaml-front-matter-endpoint)
(let ((source (md-roam-get-id))
(properties (list :outline nil)))
(while (re-search-forward md-roam-regex-link-inline nil t)
(unless (string-empty-p (match-string-no-properties 6)) ;; fix #73
(let* ((url (match-string-no-properties 6))
(parsed-url (url-generic-parse-url url))
;; If there url-type is nil then it can be a file link.
;; File links require tye type to be id for Org-roam
(type (or (url-type parsed-url) "id"))
(file-path (when (and (string-equal type "id")
;; Avoid inserting the link to DB if it's not `md-roam-file-extension'
(md-roam--markdown-file-p (url-filename parsed-url)))
;; file-path, if exists, needs to be an absolute
;; path as that's what Org-roam stores in the cache.
(buffer-file-name (find-file-noselect (url-filename parsed-url) 'NOWARN))))
;; If file-path is non-nil, check Org-roam db if it is
;; in Org-roam cache. Set ID to path. If file-path is
;; nil, get URL for refs.
;; TODO Need refactoring. path is set to nil when
;; file-path is nil (when a file is not
;; a md roam file). The behaviour is correct,
;; but it's not explicit.
(path (if file-path (md-roam-db-id-from-file-path file-path)
;; If file-path is nil, then
(when (string-match org-link-plain-re url)
(match-string-no-properties 2 url)))))
(when (and type source path)
(org-roam-db-query
[:insert :into links
:values $v1]
(vector (point) source path type properties)))))))))
(defun md-roam-db-insert-citations ()
"Insert data for citations in the current buffer into Org-roam cache.
The citation is defined in Pandoc syntax such as
\"[@citation-key]\"."
(org-with-point-at (md-roam-get-yaml-front-matter-endpoint)
(let ((source (md-roam-get-id))
;; TODO outline path always nil
(properties (list :outline nil)))
(when source
(while (re-search-forward md-roam-regex-in-text-citation-2 nil t)
(when-let
;; remove "@" for key
((key (match-string-no-properties 2)))
(org-roam-db-query
[:insert :into citations
:values $v1]
(vector source key (match-beginning 2) properties))))))))
(defun md-roam-db-insert-aliases ()
"Insert aliases in current buffer into Org-roam cache.
The aliases must be defined witin the frontmatter.
Aliases must be defined within square brakets:
[\"alias1\", \"alias of this note\"]
TODO: Other formats?"
(when-let* ((node-id (md-roam-get-id))
(frontmatter (md-roam-get-yaml-front-matter))
(aliases (and frontmatter
(string-match md-roam-regex-aliases frontmatter)
(md-roam--yaml-seq-to-list
(match-string-no-properties 2 frontmatter)))))
(org-roam-db-query [:insert :into aliases
:values $v1]
(mapcar (lambda (alias)
(vector node-id alias))
aliases))))
(defun md-roam-db-insert-refs ()
"Insert refs in current buffer into Org-roam cache.
The refs must be defined witin the frontmatter.
TODO other formats?"
(when-let* ((node-id (md-roam-get-id))
(frontmatter (md-roam-get-yaml-front-matter))
(refs (and frontmatter
(string-match md-roam-regex-ref-keys frontmatter)
(split-string-and-unquote
(match-string-no-properties 2 frontmatter)))))
(let (rows)
(dolist (ref refs)
(save-match-data
(cond ((string-match org-link-plain-re ref)
(push (vector node-id (match-string 2 ref) (match-string 1 ref)) rows))
(t
(push (vector node-id ref "cite") rows)))))
(when rows
(org-roam-db-query [:insert :into refs
:values $v1]
rows)))))
(defun md-roam-db-id-from-file-path (file-path)
"Return node ID from FILE-PATH.
FILE-PATH must be an absolute path to the file in question."
(when-let ((path (when (file-name-absolute-p file-path) file-path)))
(caar (org-roam-db-query [:select [id] :from nodes
:where (= file $s1)]
path))))
(defun md-roam-db-file-relative-path-from-id (id)
"Return file relative path from ID."
(file-relative-name
(caar (org-roam-db-query [:select [file] :from nodes
:where (= id $s1)]
id))))
;;------------------------------------------------------------------------------
;;;;; Functions for other commands: node-insert and follow-wiki-link
(cl-defun md-roam-node-insert (&optional filter-fn &key templates info)
"Find an Org-roam node and insert (where the point is) an \"id:\" link to it.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)
The INFO, if provided, is passed to the underlying `org-roam-capture-'."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
(unwind-protect
;; Group functions together to avoid inconsistent state on quit
(atomic-change-group
(let* (region-text
beg end
(_ (when (region-active-p)
(setq beg (set-marker (make-marker) (region-beginning)))
(setq end (set-marker (make-marker) (region-end)))
(setq region-text (org-link-display-format (buffer-substring-no-properties beg end)))))
(node (org-roam-node-read region-text filter-fn))
(description (or region-text
(org-roam-node-formatted node))))
(if (org-roam-node-id node)
(progn
(when region-text
(delete-region beg end)
(set-marker beg nil)
(set-marker end nil))
(insert (md-roam--wiki-link-create (org-roam-node-id node)
description
(org-roam-node-title node)))
;; for advice
t)
(org-roam-capture-
:node node
:info info
:templates templates
:props (append
(when (and beg end)
(list :region (cons beg end)))
(list :insert-at (point-marker)
:link-description description
:finalize 'insert-link)))
;; for advice
t)))
(deactivate-mark)
;; for advice
t)))
(defun md-roam-capture--finalize-insert-link ()
"Insert a wiki-link to ID when `org-roam-insert' for a new note.
This is md-roam equivalent of `org-roam-capture--finalize-insert-link'."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
(when-let* ((mkr (org-roam-capture--get :call-location))
(buf (marker-buffer mkr)))
(with-current-buffer buf
(when-let ((region (org-roam-capture--get :region)))
(org-roam-unshield-region (car region) (cdr region))
(delete-region (car region) (cdr region))
(set-marker (car region) nil)
(set-marker (cdr region) nil))
(let* ((id (org-roam-capture--get :id))
(description (org-roam-capture--get :link-description))
(link (md-roam--wiki-link-create id description description)))
(if (eq (point) (marker-position mkr))
(insert link)
(org-with-point-at mkr
(insert link)))
(run-hook-with-args 'org-roam-post-node-insert-hook
id
description))))
;; for advice
t))
(defun md-roam-follow-wiki-link (name &optional other)
"Follow wiki link NAME if there is the linked file exists.
If the linked NAME does not yet exist, call `org-roam-find-node'
to capture a new file with using the text as the title.
It is meant to advice `markdown-follow-wiki-link'.
When OTHER is non-nil by using prefix argument, open the file in
another window. This is not relevant if file does not exist."
(when (org-roam-file-p (buffer-file-name (buffer-base-buffer)))
(if-let* ((node (or (org-roam-node-from-title-or-alias name)
(org-roam-node-create :id name)))
(node-populated (org-roam-populate node))
(file (org-roam-node-file node-populated)))
(when file
(if other (find-file-other-window file) (find-file file)))
(message (format "No Org-roam node found for \"%s\"" name))
;; TODO the source file needs to be saved again to update link for the
;; backlink to be cached. This should be in the capture finaliztion
;; procss after the new buffer is saved (aborted should not be saved)
(org-roam-capture-
:node (org-roam-node-create :title name)
:props '(:finalize md-roam-find-file)))
t))
;; Capture needs to update the cache when wikilink does not have a target file.
(defun md-roam-find-file ()
"Used in`md-roam-follow-wiki-link'.
When the wiki link target file does not yet exist, Md-roam
prompts for a new file with Org-roam captuure process. When
finalizing the capture process, it updates the Org-roam cache for
the source file to cache the link from source to target."
(let ((new-file (org-roam-capture--get :new-file)))
(unless org-note-abort
(when-let ((original-buf (org-capture-get :original-buffer)))
(with-current-buffer original-buf
(md-roam-db-do-update))
(find-file new-file)))))
(defun md-roam-do-not-show-context (&optional _key)
"Used in `org-roam-node-open' to avoid error.
invalid-regexp \"Invalid regular expression\"."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
t))
;;------------------------------------------------------------------------------
;;;;; Functions for `org-roam-capture'
(defun md-roam-id-get (&rest _args)
"This is meant to replace `org-id-get' for markdown buffers.
`org-roam-capture' process tries to create and add ID in the
Org's property drawer for a new file is being created. For
markdown files, this should be prevented. We can achieve this
because currently this function does not implement the create
process \(for _create argument\).
TODO CREATE process to insert a new ID within frontmatter."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
(md-roam-get-id)))
(defun md-roam-id-at-point ()
"Return the ID at point, if any.
This function is a wrapper for `md-roam-id-get'. For md-roam, the
ID is always at the file level; headline node exits."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
(md-roam-id-get)))
(defun md-roam-capture-preface ()
"."
(advice-add #'org-entry-put :before-until #'md-roam-entry-put)
(add-hook 'org-capture-after-finalize-hook #'md-roam-capture-after-finalize)
;; Need to retrun nil for `org-roam-capture--prepare-buffer' to run the normal
;; process.
nil)
(defun md-roam-entry-put (_pom _property _value)
"Return t and do nothing when current buffer visiting markdown file.
Only for `md-roam'."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
t))
(defun md-roam-capture-after-finalize ()
"Remove the advice from `org-entry-put'."
(advice-remove #'org-entry-put #'md-roam-entry-put)
(remove-hook 'org-capture-after-finalize-hook #'md-roam-capture-after-finalize))
;;------------------------------------------------------------------------------
;;;;; Functions for `org-roam-buffer'
(defun md-roam-node-at-point (&optional _assert)
"Return the node at point.
If ASSERT, throw an error."
(when (and (buffer-file-name (buffer-base-buffer))
(md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer))))
(org-roam-populate (org-roam-node-create :id (md-roam-get-id)))))
(defun md-roam-preview-get-contents (file pt)
"Get preview content for FILE at PT."
(when (md-roam--markdown-file-p file)
(save-excursion
(org-roam-with-temp-buffer file
(org-with-wide-buffer
(goto-char pt)
(let ((beg (progn (markdown-backward-paragraph)
(point)))
(end (progn (markdown-forward-paragraph)
(point))))
(string-trim (buffer-substring-no-properties beg end))))))))
;;------------------------------------------------------------------------------
;;;;; Get functions, mainly for properties in frontmatter
(defun md-roam-get-yaml-front-matter ()
"Return the text of the YAML front matter of the current buffer.
Return nil if the front matter does not exist, or incorrectly delineated by
'---'. The front matter is required to be at the beginning of the file."
(save-excursion
(goto-char (point-min))
(when-let
((startpoint (re-search-forward
md-roam-regex-yaml-font-matter-beginning 4 t 1))
;; The beginning needs to be in the beginning of buffer
(endpoint (re-search-forward
md-roam-regex-yaml-font-matter-ending nil t 1)))
(buffer-substring-no-properties startpoint endpoint))))
(defun md-roam-get-yaml-front-matter-endpoint ()
"Return the endpoint of the YAML front matter of the current buffer.
Return nil if the front matter does not exist, or incorrectly delineated by
'---'. The front matter is required to be at the beginning of the file."
(save-excursion
(goto-char (point-min))
(when-let
((startpoint (re-search-forward
md-roam-regex-yaml-font-matter-beginning 4 t 1))
;; The beginning needs to be in the beginning of buffer
(endpoint (re-search-forward
md-roam-regex-yaml-font-matter-ending nil t 1)))
endpoint)))
(defun md-roam-get-tags ()
"Get tags defined in the Zettlr style within frontmatter."
(let ((endpoint (md-roam-get-yaml-front-matter-endpoint)))
(cond (endpoint
(save-excursion
(let (tags)
(goto-char (point-min))
(while (re-search-forward md-roam-regex-tags-zettlr-style endpoint t)
(let ((tag (match-string-no-properties 2)))
(when tag
(setq tags
(append tags
;; Remove the first char @ or #
(list (if (eql 0 (string-match "[@#]" tag))
(substring tag 1)
tag)))))))
tags))))))
(defun md-roam-get-title ()
"Extract title from the current buffer (markdown file with YAML frontmatter).
This function looks for the YAML frontmatter delineator '---' begining of
the buffer. No space is allowed before or after the delineator.
It assumes:
(1) Current buffer is a markdonw file (but does not check it)
(2) It has title in the YAML frontmatter on top of the file
(3) The format is 'title: The Document Title'"
(let ((frontmatter (md-roam-get-yaml-front-matter)))
(when (and frontmatter
(string-match md-roam-regex-title frontmatter))
(match-string-no-properties 2 frontmatter))))
(defun md-roam-get-id ()
"Extract id from the current buffer (markdown file with YAML frontmatter).
This function looks for the YAML frontmatter delineator '---' begining of
the buffer. No space is allowed before or after the delineator.
It assumes:
(1) Current buffer is a markdonw file (but does not check it)
(2) It has title in the YAML frontmatter on top of the file
(3) The format is 'id: <string>'"
(let ((frontmatter (md-roam-get-yaml-front-matter)))
(when (and frontmatter
(string-match md-roam-regex-id frontmatter))
(match-string-no-properties 2 frontmatter))))
;;------------------------------------------------------------------------------
;;;;; Completion at point
(defun md-roam-complete-wiki-link-at-point ()
"Complete wiki link at point to an existing Org-roam node.
It puts the title, not IDs."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
(let (start end)
(when (org-in-regexp org-roam-bracket-completion-re 1)
(setq start (match-beginning 2)
end (match-end 2))
(list start end
(org-roam--get-titles)
:exit-function
(lambda (&rest _)
(forward-char 2)))))))
(defun md-roam-complete-everywhere ()
"Complete symbol at point as a link completion to an Org-roam node.
This is a `completion-at-point' function, and is active when
`org-roam-completion-everywhere' is non-nil.
Unlike `org-roam-complete-link-at-point' this will complete even
outside of the bracket syntax for wiki links (i.e. \"[[|]]\"),
hence \"everywhere\"."
(when (and (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
org-roam-completion-everywhere
(thing-at-point 'word)
(not (save-match-data (org-in-regexp org-link-any-re))))
(let ((bounds (bounds-of-thing-at-point 'word)))
(list (car bounds) (cdr bounds)
(org-roam--get-titles)
:exit-function
(lambda (str _status)
(delete-char (- (length str)))
(insert "[[" str "]]"))
;; Proceed with the next completion function if the returned titles
;; do not match. This allows the default Org capfs or custom capfs
;; of lower priority to run.
:exclusive 'no))))
;;------------------------------------------------------------------------------
;;;;; Replace roam links
;; Relavant for the [[roam:]] and [[wiki link]] form
(defun md-roam--replace-roam-links-on-save-h ()
"Avoid running `org-roam-link-replace-all' on `before-save-hook'.
It's set by `org-roam-find-file-hook' and can cause an issue for
Md-roam files."
(when (md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer)))
;; do nothing for now and return t for advice
t))
;;------------------------------------------------------------------------------
;;;;; `md-roam-buffer-p' for `org-roam-buffer-list'
(defun md-roam-buffer-p (&optional buffer)
"Return t if BUFFER is for an Md-roam file.
If BUFFER is not specified, use the current buffer."
(let ((buffer (or buffer (current-buffer))))
(with-current-buffer buffer
(md-roam--markdown-file-p (buffer-file-name (buffer-base-buffer))))))
;;------------------------------------------------------------------------------
;;;;; Utility functions
;;;TODO This file can be unit tested
(defun md-roam--markdown-file-p (file)
"Return non-nil if FILE is pointing to a markdown file.
`md-roam-file-extension' can be either a string or a list of strings.
Return nil if not."
(when file
(let ((ext (org-roam--file-name-extension file)))
(if (stringp md-roam-file-extension)
(string-equal ext md-roam-file-extension)
;; If not string, it must be a list of string
(member ext md-roam-file-extension)))))
(defun md-roam--remove-single-quotes (str)
"Check if STR is surrounded by single-quotes, and remove them.
If not, return STR as is."
(let ((regexp "\\('\\)\\(.*\\)\\('\\)"))
(if (string-match regexp str)
(match-string-no-properties 2 str)
str)))
(defun md-roam--yaml-seq-to-list (seq)
"Return a list from YAML SEQ formatted in the flow style.
SEQ = sequence, It's an array. At the moment, only the flow
style works.
See the spec at https://yaml.org/spec/1.2/spec.html Flow style:
!!seq [ Clark Evans, Ingy döt Net, Oren Ben-Kiki ].
The items in the sequence (array) can be separated by different ways.
1. Spaces like the example from the spec above
2. Single-quotes 'item'
3. Double-quotes \"item\"
Do not escape the singe- or double-quotations. At the moment,
that doeslead to error.
The regexp is meant to to match YAML sequence formatted in the
flow style. At the moment, only the flow style is
considered. The number of spaces between the squeare bracket and
the first/last item should not matter.
[item1, item2, item3]
[ item1, item2, item3 ]
These should be equally valid."
(let ((regexp "\\(\\[\s*\\)\\(.*\\)\\(\s*\\]\\)")
(separator ",\s*"))
(when (string-match regexp seq)
(let ((items (split-string-and-unquote
(match-string-no-properties 2 seq) separator)))
(mapcar #'md-roam--remove-single-quotes items)))))
(defun md-roam--yaml-split-seq (seq)
"."
(let ((regexp "\\(\s*\\)\\(.*\\)\\(\s*\\)")
(separator ",\s*"))
(when (string-match regexp seq)
(let ((items (split-string-and-unquote
(match-string-no-properties 2 seq) separator)))
(mapcar #'md-roam--remove-single-quotes items)))))
(defun md-roam--wiki-link-create (id description title)
"Return wiki-link string.
This function uses `md-roam-node-insert-type' to return the
approapriate wiki-link.
ID is the node ID. DESCRIPTION is description used in
`org-roam-insert'. TITLE is the title, alias, or description of
the node being inserted."
(concat "[["
(cond
((eq md-roam-node-insert-type 'id)
(concat id "]] " description))
((eq md-roam-node-insert-type 'title-or-alias)
(concat title "]]")))))
(provide 'md-roam)
;;; md-roam.el ends here