Browse code
Add a reading list exporter
Ed Langley authored on 17/09/2018 09:30:38
Showing 7 changed files
Showing 7 changed files
- .gitignore
- build-reading-list-reader.sh
- build.lisp
- objc-data-extractors.lisp
- objc-runtime.asd
- package.lisp
- reading-list-reader.lisp
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 *)))) |