git.fiddlerwoaroof.com
Browse code

Add a reading list exporter

Ed Langley authored on 17/09/2018 09:30:38
Showing 7 changed files
... ...
@@ -31,3 +31,4 @@ NSRect-Expose
31 31
 .*.sw?
32 32
 venv
33 33
 /reading-list2org
34
+/dist/
34 35
new file mode 100644
... ...
@@ -0,0 +1,15 @@
1
+#!/usr/bin/env bash
2
+set -eu -x -o pipefail
3
+
4
+cd "$(dirname $0)"
5
+mkdir -p dist
6
+
7
+pushd dist
8
+rm -rf fwoar.lisputils
9
+git clone https://github.com/fiddlerwoaroof/fwoar.lisputils.git
10
+popd
11
+
12
+export CL_SOURCE_REGISTRY="$PWD/dist//"
13
+sbcl --no-userinit \
14
+     --load ~/quicklisp/setup.lisp \
15
+     --load build.lisp
0 16
new file mode 100644
... ...
@@ -0,0 +1,14 @@
1
+(eval-when (:compile-toplevel :load-toplevel :execute)
2
+  (setf *default-pathname-defaults* (truename "~/git_repos/objc-lisp-bridge/"))
3
+  (load (compile-file "objc-runtime.asd")))
4
+
5
+(eval-when (:compile-toplevel :load-toplevel :execute)
6
+  (ql:quickload '(:objc-runtime :yason :plump :cl-ppcre)))
7
+
8
+(load "reading-list-reader.lisp")
9
+
10
+(eval-when (:compile-toplevel :load-toplevel :execute)
11
+  (sb-ext:save-lisp-and-die "reading-list2org"
12
+                            :toplevel (intern "MAIN"
13
+                                              "READING-LIST-READER")
14
+                            :executable t))
0 15
new file mode 100644
... ...
@@ -0,0 +1,112 @@
1
+(in-package :objc-runtime.data-extractors)
2
+(named-readtables:in-readtable :objc-readtable)
3
+
4
+(defun get-plist (file)
5
+  [#@NSDictionary @(dictionaryWithContentsOfFile:)
6
+                  :pointer (objc-runtime::make-nsstring file)])
7
+
8
+(defun objc-subclass-p (sub super)
9
+  (unless (or (cffi:null-pointer-p sub)
10
+              (cffi:null-pointer-p super))
11
+    (or (eql sub super)
12
+        (= [sub @(isSubclassOfClass:) :pointer [super @(class)]]#
13
+           1))))
14
+
15
+(defun order-objc-classes (classes &rest r &key key)
16
+  (declare (ignore key))
17
+  (apply 'stable-sort
18
+         (copy-seq classes)
19
+         'objc-subclass-p
20
+         r))
21
+
22
+(defun objc-isa (obj class)
23
+  (unless (or (cffi:null-pointer-p obj)
24
+              (cffi:null-pointer-p class))
25
+    (= [obj @(isKindOfClass:) :pointer class]#
26
+       1)))
27
+
28
+(defun objc-pick-by-type (obj pairs)
29
+  (assoc obj
30
+         (order-objc-classes pairs :key 'car)
31
+         :test 'objc-isa))
32
+
33
+(serapeum:eval-always
34
+  (defun make-cases (cases obj)
35
+    (mapcar (serapeum:op
36
+              `(if (objc-isa ,obj ,(car _1))
37
+                   (progn ,@(cdr _1))))
38
+            cases)))
39
+
40
+(defmacro objc-typecase (form &body ((case-type &body case-handler) &rest cases))
41
+  (alexandria:once-only (form)
42
+    (let* ((initial-cases `((,case-type ,@case-handler) ,@(butlast cases)))
43
+           (cases (fw.lu:rollup-list (make-cases initial-cases form)
44
+                                     (if (eql t (caar (last cases)))
45
+                                         `((progn ,@(cdar (last cases))))
46
+                                         (make-cases (last cases) form)))))
47
+      cases)))
48
+
49
+(defun map-nsarray (fn arr)
50
+  (unless (and (cffi:pointerp arr)
51
+               (objc-isa arr #@NSArray))
52
+    (error "must provide a NSArray pointer"))
53
+  (loop for x below [arr @(count)]#
54
+     collect (funcall fn [arr @(objectAtIndex:) :int x])))
55
+
56
+(defun nsarray-contents (arr)
57
+  (unless (and (cffi:pointerp arr)
58
+               (objc-isa arr #@NSArray))
59
+    (error "must provide a NSArray pointer"))
60
+  (dotimes (n [arr @(count)]#)
61
+    (let ((obj [arr @(objectAtIndex:) :int n ]))
62
+      (objc-typecase obj
63
+        (#@NSString (format t "~&string~%"))
64
+        (#@NSArray (format t "~&array~%"))
65
+        (#@NSDictionary (format t "~&dictionary~%"))
66
+        (t (format t "~&other... ~s~%" (objc-runtime::objc-class-get-name
67
+                                        (objc-runtime::object-get-class obj))))))))
68
+
69
+(defvar *objc-extractors* (list)
70
+  "Functions called to extract specific data types")
71
+
72
+(serapeum:eval-always
73
+  (defun add-extractor (class cb)
74
+    (unless (member class *objc-extractors* :test 'cffi:pointer-eq :key #'car)
75
+      (setf *objc-extractors*
76
+            (merge 'list *objc-extractors* (list (cons class cb))
77
+                   'objc-subclass-p
78
+                   :key 'car)))
79
+    *objc-extractors*))
80
+
81
+(defmacro define-extractor (class (o) &body body)
82
+  `(serapeum:eval-always
83
+     (add-extractor ,class
84
+                    (lambda (,o)
85
+                      ,@body))
86
+     *objc-extractors*))
87
+
88
+(defun clear-extractors ()
89
+  (setf *objc-extractors* ()))
90
+
91
+(defmacro funcall-some (fun &rest args)
92
+  (alexandria:once-only (fun)
93
+    `(if ,fun
94
+         (funcall ,fun ,@args))))
95
+
96
+(defun extract-from-objc (obj)
97
+  (objc-typecase obj
98
+    (#@NSDate [[[[#@NSISO8601DateFormatter @(alloc)]
99
+                 @(init)]
100
+                @(stringFromDate:) :pointer obj]
101
+               @(UTF8String)]s)
102
+    (#@NSString [obj @(UTF8String)]s)
103
+    (#@NSNumber (parse-number:parse-number
104
+                 (objc-runtime::extract-nsstring
105
+                  [obj @(stringValue)])))
106
+    (#@NSArray (map-nsarray #'extract-from-objc obj))
107
+    (#@NSDictionary (fw.lu:alist-string-hash-table
108
+                     (pairlis (map-nsarray #'extract-from-objc [obj @(allKeys)])
109
+                              (map-nsarray #'extract-from-objc [obj @(allValues)]))))
110
+    (t (or (funcall-some (cdr (objc-pick-by-type obj *objc-extractors*))
111
+                         obj)
112
+           obj))))
... ...
@@ -18,4 +18,5 @@
18 18
                (:cffi-grovel-file "objc-runtime-types" :depends-on ("package"))
19 19
                (:file "readtable" :depends-on ("package"))
20 20
                (:file "gcd" :depends-on ("objc-runtime"))
21
-               (:file "objc-runtime" :depends-on ("package" "readtable" "objc-runtime-types"))))
21
+               (:file "objc-runtime" :depends-on ("package" "readtable" "objc-runtime-types"))
22
+               (:file "objc-data-extractors" :depends-on ("objc-runtime" "readtable"))))
... ...
@@ -27,3 +27,12 @@
27 27
    #:add-pointer-ivar
28 28
    #:objc-msg-send-int
29 29
    #:objc-msg-send-string))
30
+
31
+(defpackage :objc-runtime.data-extractors
32
+  (:use :cl )
33
+  (:export
34
+   #:extract-from-objc
35
+   #:define-extractor
36
+   #:clear-extractors
37
+   #:add-extractor
38
+   #:get-plist))
... ...
@@ -1,100 +1,33 @@
1
-#+build
2
-(eval-when (:compile-toplevel :load-toplevel :execute)
3
-  (setf *default-pathname-defaults* (truename "~/git_repos/objc-lisp-bridge/"))
4
-  (load (compile-file "objc-runtime.asd")))
5
-
6
-#+build
7
-(eval-when (:compile-toplevel :load-toplevel :execute)
8
-  (ql:quickload '(:objc-runtime :yason :plump :cl-ppcre)))
9
-
10 1
 (defpackage :reading-list-reader
11 2
   (:use :cl )
12 3
   (:export ))
13 4
 (in-package :reading-list-reader)
14 5
 
15 6
 (serapeum:eval-always
16
- (named-readtables:in-readtable :objc-readtable))
17
-
18
-(defparameter *reading-list-location* "~/Library/Safari/Bookmarks.plist")
19
-
20
-(defun get-plist (file)
21
-  [#@NSDictionary @(dictionaryWithContentsOfFile:)
22
-                  :pointer (objc-runtime::make-nsstring file)])
23
-
24
-
25
-(defun objc-isa (obj class)
26
-  (unless (or (cffi:null-pointer-p obj)
27
-              (cffi:null-pointer-p class))
28
-    (= [obj @(isKindOfClass:) :pointer class]#
29
-       1)))
30
-
31
-(serapeum:eval-always
32
-  (defun make-cases (cases obj)
33
-    (mapcar (serapeum:op
34
-              `(if (objc-isa ,obj ,(car _1))
35
-                   (progn ,@(cdr _1))))
36
-            cases)))
37
-
38
-(defmacro objc-typecase (form &body ((case-type &body case-handler) &rest cases))
39
-  (alexandria:once-only (form)
40
-    (let* ((initial-cases `((,case-type ,@case-handler) ,@(butlast cases)))
41
-           (cases (fw.lu:rollup-list (make-cases initial-cases form)
42
-                                     (if (eql t (caar (last cases)))
43
-                                         `((progn ,@(cdar (last cases))))
44
-                                         (make-cases (last cases) form)))))
45
-      cases)))
7
+  (named-readtables:in-readtable :objc-readtable))
46 8
 
47
-(defun map-nsarray (fn arr)
48
-  (unless (and (cffi:pointerp arr)
49
-               (objc-isa arr #@NSArray))
50
-    (error "must provide a NSArray pointer"))
51
-  (loop for x below [arr @(count)]#
52
-       collect (funcall fn [arr @(objectAtIndex:) :int x])))
53
-
54
-(defun nsarray-contents (arr)
55
-  (unless (and (cffi:pointerp arr)
56
-               (objc-isa arr #@NSArray))
57
-    (error "must provide a NSArray pointer"))
58
-  (dotimes (n [arr @(count)]#)
59
-    (let ((obj [arr @(objectAtIndex:) :int n ]))
60
-      (objc-typecase obj
61
-        (#@NSString (format t "~&string~%"))
62
-        (#@NSArray (format t "~&array~%"))
63
-        (#@NSDictionary (format t "~&dictionary~%"))
64
-        (t (format t "~&other... ~s~%" (objc-runtime::objc-class-get-name
65
-                                        (objc-runtime::object-get-class obj))))))))
66
-
67
-(defun extract-from-objc (obj)
68
-  (objc-typecase obj
69
-    (#@NSDate [[[[#@NSISO8601DateFormatter @(alloc)]
70
-                 @(init)]
71
-                @(stringFromDate:) :pointer obj]
72
-               @(UTF8String)]s)
73
-    (#@NSString [obj @(UTF8String)]s)
74
-    (#@NSNumber (parse-number:parse-number
75
-                 (objc-runtime::extract-nsstring
76
-                  [obj @(stringValue)])))
77
-    (#@NSArray (map-nsarray #'extract-from-objc obj))
78
-    (#@NSDictionary (fw.lu:alist-string-hash-table
79
-                     (pairlis (map-nsarray #'extract-from-objc [obj @(allKeys)])
80
-                              (map-nsarray #'extract-from-objc [obj @(allValues)]))))
81
-    (t (format *error-output* "~&other... ~s~%" (objc-runtime::objc-class-get-name
82
-                                                 (objc-runtime::object-get-class obj))))))
9
+(defun main ()
10
+  #+(and build sbcl)
11
+  (progn (sb-ext:disable-debugger)
12
+         (sb-alien:alien-funcall
13
+          (sb-alien:extern-alien "disable_lossage_handler"
14
+                                 (function sb-alien:void))))
15
+  (make-org-file *standard-output*
16
+                 (translate-plist (get-bookmark-filename))))
83 17
 
84
-(defun extract-nsdictionary (nsdict)
85
-  (yason:with-output (*standard-output* :indent t)
86
-    (maphash 'yason:encode-object-element
87
-             (extract-from-objc nsdict))))
18
+(defparameter *reading-list-location* "Library/Safari/Bookmarks.plist")
19
+(defun get-bookmark-filename ()
20
+  (merge-pathnames *reading-list-location*
21
+                   (truename "~/")))
88 22
 
89
-(defun select-child (d title)
90
-  (remove-if-not (serapeum:op
91
-                   (equal (gethash "Title" _)
92
-                          title))
93
-                 (gethash "Children" d)))
23
+(defun translate-plist (fn)
24
+  (objc-runtime.data-extractors:extract-from-objc
25
+   (objc-runtime.data-extractors:get-plist fn)))
94 26
 
95
-(defun slugify (s)
96
-  (when s
97
-    (cl-ppcre:regex-replace-all "\\s+" (string-downcase s) "_")))
27
+(defun make-org-file (s bookmarks)
28
+  (format s "~&* Safari Reading List~%")
29
+  (serapeum:mapply (serapeum:partial 'make-org-entry s)
30
+                   (get-readinglist-info bookmarks)))
98 31
 
99 32
 (defun make-org-entry (s title url preview tag)
100 33
   (format s "~&** ~a :~{~a:~}~% ~a~2% ~{~<~% ~1,80:;~a~> ~}~%"
... ...
@@ -104,29 +37,37 @@
104 37
 
105 38
 (defun get-readinglist-info (bookmarks)
106 39
   (mapcar (serapeum:juxt
107
-           (fw.lu:op (fw.lu:pick '("URIDictionary" "title") _))
108
-           (fw.lu:op (fw.lu:pick '("URLString") _))
109
-           (fw.lu:op (plump:decode-entities (coerce (fw.lu:pick '("ReadingList" "PreviewText") _)
110
-                                                    'simple-string)
111
-                                            t))
112
-           (fw.lu:op (slugify (fw.lu:pick '("ReadingListNonSync" "siteName") _))))
40
+           (fw.lu:op
41
+             (fw.lu:pick '("URIDictionary" "title")
42
+                         _))
43
+           (fw.lu:op
44
+             (fw.lu:pick '("URLString")
45
+                         _))
46
+           (fw.lu:op
47
+             (plump:decode-entities
48
+              (coerce (fw.lu:pick '("ReadingList" "PreviewText")
49
+                                  _)
50
+                      'simple-string)
51
+              t))
52
+           (fw.lu:op
53
+             (fw.lu:may
54
+               (slugify
55
+                (fw.lu:pick '("ReadingListNonSync" "siteName")
56
+                            _)))))
113 57
           (gethash "Children"
114 58
                    (car
115
-                    (select-child bookmarks "com.apple.ReadingList")))))
116
-
59
+                    (select-child bookmarks
60
+                                  "com.apple.ReadingList")))))
117 61
 
118
-(defun make-org-file (s bookmarks)
119
-  (format s "~&* Safari Reading List~%")
120
-  (serapeum:mapply (serapeum:partial 'make-org-entry s)
121
-                   (get-readinglist-info bookmarks)))
122
-
123
-(defun main ()
124
-  #+(and build sbcl)
125
-  (progn (sb-ext:disable-debugger)
126
-         (sb-alien:alien-funcall
127
-          (sb-alien:extern-alien "disable_lossage_handler" (function sb-alien:void))))
128
-  (make-org-file *standard-output*
129
-                 (extract-from-objc (get-plist (uiop:unix-namestring (truename *reading-list-location*))))))
62
+(defun slugify (s)
63
+  (cl-ppcre:regex-replace-all "\\s+"
64
+                              (string-downcase s)
65
+                              "_"))
130 66
 
131
-#+build
132
-(sb-ext:save-lisp-and-die "reading-list2org" :toplevel 'main :executable t)
67
+(defun select-child (d title)
68
+  (flet ((get-title (h)
69
+           (equal (gethash "Title" h)
70
+                  title)))
71
+    (fw.lu:let-each (:be *)
72
+      (gethash "Children" d)
73
+      (remove-if-not #'get-title *))))