64
|
1
|
;;; organ.lisp --- Org parser |
93
|
2
|
(defpackage :organ |
96
|
3
|
(:use :cl :cl-ppcre :std) |
64
|
4
|
(:shadowing-import-from :sb-gray :fundamental-stream) |
|
5
|
(:import-from :uiop :read-file-string) |
|
6
|
(:export |
|
7
|
;; params |
|
8
|
:*org-todo-keyword-types* |
|
9
|
:*org-todo-keywords* |
|
10
|
;; vars |
|
11
|
:org-todo-keyword-map |
|
12
|
:org-heading-regexp |
|
13
|
:org-file-property-regexp |
|
14
|
:org-todo-keyword-regexp |
|
15
|
:org-property-regexp |
|
16
|
:org-tag-regexp |
|
17
|
:org-element-types |
|
18
|
:org-element-objects |
|
19
|
;; proto |
|
20
|
:org-parse |
|
21
|
:org-parse-lines |
|
22
|
;; classes |
|
23
|
:org-element |
|
24
|
:text |
|
25
|
:org-kind |
|
26
|
:org-file |
|
27
|
:org-lines |
|
28
|
:o-lines |
|
29
|
:org-stream |
|
30
|
:org-headline |
|
31
|
:level |
|
32
|
:props |
|
33
|
:tags |
|
34
|
:title |
|
35
|
:state |
|
36
|
:org-todo-keyword |
|
37
|
:todo-type |
|
38
|
:org-list |
|
39
|
:org-tag |
|
40
|
;; fns |
|
41
|
:org-todo-keyword-p |
|
42
|
:org-tag-split |
|
43
|
:read-org-file |
|
44
|
:read-org-lines |
|
45
|
:read-org-lines-from-string |
|
46
|
:make-org-headline |
|
47
|
:make-org-todo-keyword |
|
48
|
:make-org-tag |
|
49
|
;; macs |
|
50
|
:org-init)) |
|
51
|
|
|
52
|
(in-package :organ) |
|
53
|
|
|
54
|
(defparameter *org-todo-keyword-types* |
|
55
|
'(todo wip done)) |
|
56
|
|
|
57
|
(defparameter *org-todo-keywords* |
|
58
|
'(("TODO" todo) ("DONE" done) ("FIND" todo) ("FOUND" done) |
|
59
|
("RESEARCH" todo) ("RECORD" todo) ("OUTLINE" todo) ("DRAFT" todo) |
|
60
|
("REVIEW" todo) ("FIX" todo) ("IMPL" todo) ("TEST" todo) ("FIXED" done) |
|
61
|
("GOTO" todo) ("HACK" todo) ("NOTE" todo) ("CODE" todo) ("LINK" todo)) |
|
62
|
"List of keywords accepted by `organ'. ") |
|
63
|
|
|
64
|
(defun org-todo-keyword-map () |
|
65
|
(let ((kws (make-hash-table :size 20))) |
|
66
|
(dolist (kw *org-todo-keywords*) |
|
67
|
(let ((k (intern (car kw))) |
|
68
|
(v (cadr kw))) |
|
69
|
(assert (member v *org-todo-keyword-types*)) |
|
70
|
(setf (gethash k kws) v))) |
|
71
|
kws)) |
|
72
|
|
|
73
|
(defvar org-todo-keyword-map (org-todo-keyword-map)) |
|
74
|
|
|
75
|
(defmacro org-todo-keyword-p (kw) |
|
76
|
"Search for symbol KW in `org-todo-keyword-map' returning the |
|
77
|
associated value or nil if not found." |
|
78
|
`(gethash (intern ,kw) org-todo-keyword-map)) |
|
79
|
|
93
|
80
|
(defvar org-headline-regexp "^([*]+)\\s+(.*)$") |
|
81
|
(defvar org-todo-keyword-regexp "^(\\w+)\\s+(.*)$") |
|
82
|
(defvar org-file-property-regexp "^[#+](.*)[:]\\s+(.*)$") |
|
83
|
(defvar org-property-regexp "^[:](.*)[:]\\s+(.*)$") |
|
84
|
|
64
|
85
|
;; this doesn't consume leading whitespace. It could be useful in the |
|
86
|
;; future to infer a value for org-tags-column but is contained in the |
|
87
|
;; title slot of `org-headline' for now. The result of this scan is a |
|
88
|
;; single string delimited by the ':' character. To get a list of tags |
|
89
|
;; as strings, use `org-tag-split'. |
93
|
90
|
(defvar org-tag-regexp "(:[\\w_@#%:]+:)$") |
64
|
91
|
|
|
92
|
(defun org-tag-split (tags) |
|
93
|
(remove-if (lambda (s) (typep s '(string 0))) (cl-ppcre:split ":" tags))) |
|
94
|
|
|
95
|
(defvar org-element-types |
|
96
|
'(babel-call center-block clock comment comment-block diary-sexp drawer |
|
97
|
dynamic-block example-block export-block fixed-width footnote-definition |
|
98
|
headline horizontal-rule inlinetask item keyword latex-environment |
|
99
|
node-property paragraph plain-list planning property-drawer quote-block |
|
100
|
section special-block src-block table table-row verse-block) |
|
101
|
"List of all org-element types provided by org-element.el in 'org-element-all-elements'") |
|
102
|
|
|
103
|
(defvar org-element-objects |
|
104
|
'(bold citation citation-reference code entity export-snippet |
|
105
|
footnote-reference inline-babel-call inline-src-block italic |
|
106
|
line-break latex-fragment link macro radio-target statistics-cookie |
|
107
|
strike-through subscript superscript table-cell target timestamp underline verbatim) |
|
108
|
"List of all org-element objects provided by org-element.el in 'org-element-all-objects'") |
|
109
|
|
|
110
|
(defgeneric org-parse (self) |
|
111
|
(:documentation "Parse the text slot from ORG-ELEMENT.")) |
|
112
|
|
|
113
|
(defgeneric org-parse-lines (self) |
|
114
|
(:documentation "Parse the text slot from ORG-ELEMENT as a vector of lines.")) |
|
115
|
|
|
116
|
(defmacro org-init (class &optional text) |
|
117
|
"Initialize a instance of `org-element' CLASS with optional TEXT." |
|
118
|
`(make-instance ',class :text ,(or text ""))) |
|
119
|
|
|
120
|
;; parent and children are implicit. A single instance of |
|
121
|
;; `org-element' contains a complete org-mode AST. |
|
122
|
(defclass org-element () |
|
123
|
((text :initarg :text :accessor text :type string) |
|
124
|
(kind :initarg :kind :accessor org-kind :type keyword))) |
|
125
|
|
|
126
|
(defmethod org-parse-lines ((self org-element)) |
|
127
|
(let ((lines (o-lines (read-org-lines-from-string (slot-value self 'text))))) |
|
128
|
(loop for i from 1 for x across lines |
|
129
|
collect |
|
130
|
(if (cl-ppcre:scan org-headline-regexp x) (list i (symb 'headline) x) |
|
131
|
(if (cl-ppcre:scan org-file-property-regexp x) (list i 'file-property x) |
|
132
|
(if (cl-ppcre:scan org-property-regexp x) (list i 'node-property x) |
|
133
|
(list i nil x))))))) |
|
134
|
|
|
135
|
(defclass org-stream (fundamental-stream) |
|
136
|
((stream :initarg :stream :reader stream-of))) |
|
137
|
|
|
138
|
(defclass org-file (org-element org-stream) |
|
139
|
((path :initarg :path :accessor path) |
|
140
|
(kind :allocation :class :initform :file))) |
|
141
|
|
|
142
|
(defun read-org-file (path) |
|
143
|
(make-instance 'org-file :path path :text (read-file-string path))) |
|
144
|
|
|
145
|
;; (slot-value (read-org-file "~/org/notes.org") 'text) |
|
146
|
|
|
147
|
(defclass org-lines (org-element) |
|
148
|
((lines :initarg :lines :type vector :accessor o-lines) |
|
149
|
(kind :allocation :class :initform :org-lines))) |
|
150
|
|
|
151
|
(defun read-org-lines (&optional stream) |
|
152
|
(let ((slice (make-instance 'org-lines))) |
|
153
|
(setf (o-lines slice) |
|
154
|
(apply #'vector |
|
155
|
(loop for l = (read-line stream nil :eof) |
|
156
|
until (eq l :eof) |
|
157
|
collect l))) |
|
158
|
slice)) |
|
159
|
|
|
160
|
(defun read-org-lines-from-string (str) |
|
161
|
(with-input-from-string (s str) (read-org-lines s))) |
|
162
|
|
|
163
|
;; when level=0, headline is uninitialized |
|
164
|
(defclass org-headline (org-element) |
|
165
|
((kind :allocation :class :initform :org-headline) |
|
166
|
(state :accessor state :initform nil) |
|
167
|
(level :accessor level :initform 0) |
|
168
|
(props :accessor props :initform nil) |
|
169
|
(priority :accessor priority :initform nil) |
|
170
|
(tags :accessor tags :initform nil) |
|
171
|
(title :accessor title :initform ""))) |
|
172
|
|
|
173
|
(defun make-org-headline (text) |
|
174
|
(org-init org-headline text)) |
|
175
|
|
|
176
|
(defmethod org-parse ((self org-headline)) |
|
177
|
(with-input-from-string (s (text self)) |
|
178
|
(when (peek-char #\* s) ;; start headline |
|
179
|
(let ((line (read-line s))) |
|
180
|
(multiple-value-bind (start _ reg-start reg-end) |
|
181
|
;; scan for headline |
|
182
|
(cl-ppcre:scan org-headline-regexp line) |
|
183
|
(declare (ignore _)) |
|
184
|
(when start |
|
185
|
(loop for rs across reg-start |
|
186
|
for re across reg-end |
|
187
|
for i from 0 |
|
188
|
do |
|
189
|
(if (= i 0) |
|
190
|
(setf (level self) (- re rs)) |
|
191
|
(let ((sub (subseq line rs))) |
|
192
|
(multiple-value-bind (match subs) |
|
193
|
;; scan for todo-keyword |
|
194
|
(cl-ppcre:scan-to-strings org-todo-keyword-regexp sub) |
|
195
|
(if match |
|
196
|
(let ((k (svref subs 0))) |
|
197
|
(if (org-todo-keyword-p k) |
|
198
|
(setf (state self) (make-org-todo-keyword k) |
|
199
|
(title self) (svref subs 1)) |
|
200
|
(setf (title self) match))) |
|
201
|
(setf (title self) sub)))))))) |
|
202
|
;; scan for tags, modifies title slot |
|
203
|
(let ((tag-str (cl-ppcre:scan-to-strings org-tag-regexp (title self)))) |
|
204
|
(when tag-str |
|
205
|
(setf (tags self) (apply #'vector (mapcar #'make-org-tag (org-tag-split tag-str))) |
|
206
|
(title self) (subseq (title self) 0 (- (length (title self)) (length tag-str)))))))) |
|
207
|
;; TODO 2023-07-24: cookies,priority |
|
208
|
self)) |
|
209
|
|
|
210
|
(defclass org-todo-keyword (org-element) |
|
211
|
((kind :allocation :class :initform :org-todo-keyword) |
|
212
|
(todo-type :accessor todo-type :initarg :type :initform nil :type symbol))) |
|
213
|
|
|
214
|
(defun make-org-todo-keyword (text &optional type) |
|
215
|
(make-instance 'org-todo-keyword :text text :type type)) |
|
216
|
|
|
217
|
(defmethod org-parse ((self org-todo-keyword)) |
|
218
|
(let* ((text (text self)) |
|
219
|
(type (gethash (intern text) org-todo-keyword-map nil))) |
|
220
|
(when type (setf (todo-type self) type)) |
|
221
|
self)) |
|
222
|
|
|
223
|
(defclass org-list (org-element) |
|
224
|
((kind :allocation :class :initform :org-list))) |
|
225
|
|
|
226
|
(defclass org-tag (org-element) |
|
227
|
((kind :allocation :class :initform :org-tag))) |
|
228
|
|
|
229
|
(defun make-org-tag (text) (org-init org-tag text)) |
|
230
|
|
|
231
|
(defmethod org-parse ((self org-tag)) self) ;; nop |
|
232
|
|
|
233
|
(defclass org-block (org-element) ()) |
|
234
|
|
|
235
|
(defclass org-paragraph (org-element) ()) |
|
236
|
|
|
237
|
(provide :organ) |