瀏覽代碼

fix(pdf): conflicts

charlie 4 年之前
父節點
當前提交
0284d697c8
共有 41 個文件被更改,包括 1888 次插入255 次删除
  1. 3 1
      deps.edn
  2. 1 1
      resources/package.json
  3. 10 3
      src/main/frontend/commands.cljs
  4. 4 1
      src/main/frontend/components/block.cljs
  5. 4 0
      src/main/frontend/components/block.css
  6. 35 27
      src/main/frontend/components/editor.cljs
  7. 1 1
      src/main/frontend/components/editor.css
  8. 1 1
      src/main/frontend/components/onboarding.cljs
  9. 18 1
      src/main/frontend/components/settings.cljs
  10. 13 4
      src/main/frontend/date.cljs
  11. 14 0
      src/main/frontend/db_schema.cljs
  12. 28 27
      src/main/frontend/diff.cljs
  13. 256 0
      src/main/frontend/extensions/zotero.cljs
  14. 36 0
      src/main/frontend/extensions/zotero.css
  15. 156 0
      src/main/frontend/extensions/zotero/api.cljs
  16. 158 0
      src/main/frontend/extensions/zotero/extractor.cljs
  17. 84 0
      src/main/frontend/extensions/zotero/handler.cljs
  18. 15 0
      src/main/frontend/extensions/zotero/schema.cljs
  19. 37 0
      src/main/frontend/extensions/zotero/setting.cljs
  20. 18 2
      src/main/frontend/format/block.cljs
  21. 6 1
      src/main/frontend/format/mldoc.cljs
  22. 55 29
      src/main/frontend/handler/editor.cljs
  23. 2 1
      src/main/frontend/handler/editor/lifecycle.cljs
  24. 17 15
      src/main/frontend/handler/extract.cljs
  25. 63 25
      src/main/frontend/handler/page.cljs
  26. 19 16
      src/main/frontend/handler/repo.cljs
  27. 2 1
      src/main/frontend/handler/web/nfs.cljs
  28. 1 3
      src/main/frontend/modules/file/core.cljs
  29. 11 6
      src/main/frontend/routes.cljs
  30. 13 2
      src/main/frontend/state.cljs
  31. 10 11
      src/main/frontend/text.cljs
  32. 27 6
      src/main/frontend/util.cljc
  33. 67 63
      src/main/frontend/util/property.cljs
  34. 1 1
      src/main/frontend/version.cljs
  35. 285 0
      src/test/fixtures/zotero.edn
  36. 2 2
      src/test/frontend/db/query_dsl_test.cljs
  37. 73 0
      src/test/frontend/extensions/zotero/extractor_test.cljs
  38. 1 1
      src/test/frontend/format/block_test.cljs
  39. 3 3
      src/test/frontend/util/property_test.cljs
  40. 331 0
      templates/zotero-items.edn
  41. 7 0
      yarn.lock

+ 3 - 1
deps.edn

@@ -21,7 +21,9 @@
   ;; fork
   com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time",
                                :sha "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
-  cljs-drag-n-drop/cljs-drag-n-drop {:mvn/version "0.1.0"}
+  cljs-drag-n-drop/cljs-drag-n-drop
+  {:mvn/version "0.1.0"}
+  cljs-http/cljs-http {:mvn/version "0.1.46"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
   hickory/hickory             {:mvn/version "0.7.1"}
   hiccups/hiccups             {:mvn/version "0.3.0"}

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.2.10",
+  "version": "0.3.0",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",

+ 10 - 3
src/main/frontend/commands.cljs

@@ -37,6 +37,9 @@
                                        :id :label
                                        :placeholder "Label"}]]])
 
+(def zotero-steps [[:editor/input (str slash "zotero")]
+                   [:editor/show-zotero]])
+
 (def *extend-slash-commands (atom []))
 
 (defn register-slash-command [cmd]
@@ -249,6 +252,7 @@
     ;; advanced
 
     [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]] "Create a DataScript query"]
+     ["Zotero" zotero-steps "Import Zotero journal article"]
      ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
@@ -331,9 +335,9 @@
                      (or backward-pos 0))]
       (state/set-block-content-and-last-pos! id new-value new-pos)
       (cursor/move-cursor-to input
-                           (if (or backward-pos forward-pos)
-                             new-pos
-                             (+ new-pos 1))))))
+                             (if (or backward-pos forward-pos)
+                               new-pos
+                               (+ new-pos 1))))))
 
 (defn simple-insert!
   [id value
@@ -544,6 +548,9 @@
 (defmethod handle-step :editor/show-input [[_ option]]
   (state/set-editor-show-input! option))
 
+(defmethod handle-step :editor/show-zotero [[_]]
+  (state/set-editor-show-zotero! true))
+
 (defmethod handle-step :editor/show-date-picker [[_ type]]
   (if (and
        (contains? #{:scheduled :deadline} type)

+ 4 - 1
src/main/frontend/components/block.cljs

@@ -1506,7 +1506,7 @@
   (let [pre-block? (:block/pre-block? block)
         date (and (= k :date) (date/get-locale-string (str v)))]
     [:div
-     [:span.font-bold (name k)]
+     [:span.page-property-key.font-medium (name k)]
      [:span.mr-1 ":"]
      (cond
        (int? v)
@@ -1546,6 +1546,9 @@
                            (assoc properties :alias aliases))
                          properties))
                      properties)
+        properties-order (if pre-block?
+                           (remove #{:title :filters} properties-order)
+                           properties-order)
         properties (if (seq properties-order)
                      (map (fn [k] [k (get properties k)]) properties-order)
                      properties)]

+ 4 - 0
src/main/frontend/components/block.css

@@ -474,3 +474,7 @@ span.cloze-revealed {
     text-decoration: underline;
     text-underline-position: under;
 }
+
+.page-property-key {
+  color: var(--ls-secondary-text-color);
+}

+ 35 - 27
src/main/frontend/components/editor.cljs

@@ -14,6 +14,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
+            [frontend.extensions.zotero :as zotero]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -31,6 +32,7 @@
                (not (state/sub :editor/show-block-search?))
                (not (state/sub :editor/show-template-search?))
                (not (state/sub :editor/show-input))
+               (not (state/sub :editor/show-zotero))
                (not (state/sub :editor/show-date-picker?)))
       (let [matched (util/react *matched-commands)]
         (ui/auto-complete
@@ -98,21 +100,21 @@
               matched-pages (when-not (string/blank? q)
                               (editor-handler/get-matched-pages q))]
           (ui/auto-complete
-            matched-pages
-            {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
-             :on-enter    #(page-handler/page-not-exists-handler input id q current-pos)
-             :item-render (fn [page-name chosen?]
-                            [:div.py-2.preview-trigger-wrapper
-                             (block/page-preview-trigger
-                               {:children        [:div (search/highlight-exact-query page-name q)]
-                                :open?           chosen?
-                                :manual?         true
-                                :fixed-position? true
-                                :tippy-distance  24
-                                :tippy-position  (if sidebar? "left" "right")}
-                               page-name)])
-             :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
-             :class       "black"}))))))
+           matched-pages
+           {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
+            :on-enter    #(page-handler/page-not-exists-handler input id q current-pos)
+            :item-render (fn [page-name chosen?]
+                           [:div.py-2.preview-trigger-wrapper
+                            (block/page-preview-trigger
+                             {:children        [:div (search/highlight-exact-query page-name q)]
+                              :open?           chosen?
+                              :manual?         true
+                              :fixed-position? true
+                              :tippy-distance  24
+                              :tippy-position  (if sidebar? "left" "right")}
+                             page-name)])
+            :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
+            :class       "black"}))))))
 
 (rum/defcs block-search-auto-complete < rum/reactive
   {:init (fn [state]
@@ -289,7 +291,7 @@
 
 (rum/defc absolute-modal < rum/static
   [cp set-default-width? {:keys [top left rect]}]
-  (let [max-height 300
+  (let [max-height 370
         max-width 300
         offset-top 24
         vw-height js/window.innerHeight
@@ -388,17 +390,17 @@
 (defn get-editor-heading-class [content]
   (let [content (if content (str content) "")]
     (cond
-     (string/includes? content "\n") "multiline-block"
-     (starts-with? content "# ") "h1"
-     (starts-with? content "## ") "h2"
-     (starts-with? content "### ") "h3"
-     (starts-with? content "#### ") "h4"
-     (starts-with? content "##### ") "h5"
-     (starts-with? content "###### ") "h6"
-     (starts-with? content "TODO ") "todo-block"
-     (starts-with? content "DOING ") "doing-block"
-     (starts-with? content "DONE ") "done-block"
-     :else "normal-block")))
+      (string/includes? content "\n") "multiline-block"
+      (starts-with? content "# ") "h1"
+      (starts-with? content "## ") "h2"
+      (starts-with? content "### ") "h3"
+      (starts-with? content "#### ") "h4"
+      (starts-with? content "##### ") "h5"
+      (starts-with? content "###### ") "h6"
+      (starts-with? content "TODO ") "todo-block"
+      (starts-with? content "DOING ") "doing-block"
+      (starts-with? content "DONE ") "done-block"
+      :else "normal-block")))
 
 (rum/defc mock-textarea
   < rum/reactive
@@ -500,5 +502,11 @@
       true
       *slash-caret-pos)
 
+     (when (state/sub :editor/show-zotero)
+       (transition-cp
+        (zotero/zotero-search id)
+        false
+        *slash-caret-pos))
+
      (when format
        (image-uploader id format))]))

+ 1 - 1
src/main/frontend/components/editor.css

@@ -67,4 +67,4 @@ pre {
 /* Fix autocomplete preview  */
 .preview-trigger-wrapper > [data-tooltipped] {
   display: block !important;
-}
+}

+ 1 - 1
src/main/frontend/components/onboarding.cljs

@@ -237,7 +237,7 @@
         :class "text-sm p-1 ml-3"
         :on-click
         (fn []
-          (route-handler/redirect! {:to :shortcut})))
+          (route-handler/redirect! {:to :shortcut-setting})))
        (shortcut/trigger-table)
        (shortcut/shortcut-table :shortcut.category/basics)
        (shortcut/shortcut-table :shortcut.category/block-editing)

+ 18 - 1
src/main/frontend/components/settings.cljs

@@ -372,7 +372,23 @@
       :on-click
       (fn []
         (state/close-settings!)
-        (route-handler/redirect! {:to :shortcut})))]]])
+        (route-handler/redirect! {:to :shortcut-setting})))]]])
+
+(defn zotero-settings-row [t]
+  [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
+   [:label.block.text-sm.font-medium.leading-5.opacity-70
+    {:for "zotero_settings"}
+    "Zotero settings"]
+   [:div.mt-1.sm:mt-0.sm:col-span-2
+    [:div
+     (ui/button
+      "Zotero settings"
+      :class "text-sm p-1"
+      :style {:margin-top "0px"}
+      :on-click
+      (fn []
+        (state/close-settings!)
+        (route-handler/redirect! {:to :zotero-setting})))]]])
 
 (defn auto-push-row [t current-repo enable-git-auto-push?]
   (when (string/starts-with? current-repo "https://")
@@ -474,6 +490,7 @@
         (enable-all-pages-public-row t enable-all-pages-public?)
         (encryption-row t enable-encryption?)
         (keyboard-shortcuts-row t)
+        (zotero-settings-row t)
         (auto-push-row t current-repo enable-git-auto-push?)]
 
        [:hr] ;; Outside of panel wrap so that it is wider

+ 13 - 4
src/main/frontend/date.cljs

@@ -1,14 +1,15 @@
 (ns frontend.date
-  (:require [cljs-time.core :as t]
+  (:require ["chrono-node" :as chrono]
+            [cljs-bean.core :as bean]
             [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
             [cljs-time.format :as tf]
             [cljs-time.local :as tl]
+            [clojure.string :as string]
             [frontend.state :as state]
-            [cljs-bean.core :as bean]
             [frontend.util :as util]
-            [clojure.string :as string]
             [goog.object :as gobj]
-            ["chrono-node" :as chrono]))
+            [lambdaisland.glogi :as log]))
 
 (defn nld-parse
   [s]
@@ -114,6 +115,14 @@
   ([date]
    (format date)))
 
+(defn journal-name-s [s]
+  (try
+    (journal-name (tf/parse (tf/formatter "yyyy-MM-dd") s))
+    (catch js/Error e
+      (log/info :parse-journal-date {:message  "Unable to parse date to journal name, skipping."
+                                     :date-str s})
+      nil)))
+
 (defn today
   []
   (journal-name))

+ 14 - 0
src/main/frontend/db_schema.cljs

@@ -147,3 +147,17 @@
     :block/updated-at
     }
   )
+
+
+;;; use `(map [:db.fn/retractAttribute <id> <attr>] retract-page-attributes)`
+;;; to remove attrs to make the page as it's just created and no file attached to it
+(def retract-page-attributes
+  #{:block/created-at
+    :block/updated-at
+    :block/file
+    :block/format
+    :block/content
+    :block/properties
+    :block/alias
+    :block/tags
+    :block/unordered})

+ 28 - 27
src/main/frontend/diff.cljs

@@ -33,30 +33,31 @@
 ;; (find-position "** hello _w_" "hello w")
 (defn find-position
   [markup text]
-  (try
-    (let [pos (loop [t1 (-> markup string/lower-case seq)
-                     t2 (-> text   string/lower-case seq)
-                     i1 0
-                     i2 0]
-                (let [[h1 & r1] t1
-                      [h2 & r2] t2]
-                  (cond
-                    (or (empty? t1) (empty? t2))
-                    i1
-
-                    (= h1 h2)
-                    (recur r1 r2 (inc i1) (inc i2))
-
-                    (#{\[ \space \]} h2)
-                    (recur t1 r2 i1 (inc i2))
-
-                    :else
-                    (recur r1 t2 (inc i1) i2))))]
-      (if (and (= (util/nth-safe markup pos)
-                  (util/nth-safe markup (inc pos))
-                  "]"))
-        (+ pos 2)
-        pos))
-    (catch js/Error e
-      (log/error :diff/find-position {:error e})
-      (count markup))))
+  (when (and (string? markup) (string? text))
+    (try
+      (let [pos (loop [t1 (-> markup string/lower-case seq)
+                       t2 (-> text   string/lower-case seq)
+                       i1 0
+                       i2 0]
+                  (let [[h1 & r1] t1
+                        [h2 & r2] t2]
+                    (cond
+                      (or (empty? t1) (empty? t2))
+                      i1
+
+                      (= h1 h2)
+                      (recur r1 r2 (inc i1) (inc i2))
+
+                      (#{\[ \space \]} h2)
+                      (recur t1 r2 i1 (inc i2))
+
+                      :else
+                      (recur r1 t2 (inc i1) i2))))]
+        (if (and (= (util/nth-safe markup pos)
+                    (util/nth-safe markup (inc pos))
+                    "]"))
+          (+ pos 2)
+          pos))
+      (catch js/Error e
+        (log/error :diff/find-position {:error e})
+        (count markup)))))

+ 256 - 0
src/main/frontend/extensions/zotero.cljs

@@ -0,0 +1,256 @@
+(ns frontend.extensions.zotero
+  (:require [cljs.core.async :refer [<! >! go chan go-loop] :as a]
+            [clojure.string :as str]
+            [frontend.components.svg :as svg]
+            [frontend.extensions.zotero.api :as api]
+            [frontend.extensions.zotero.handler :as zotero-handler]
+            [frontend.extensions.zotero.setting :as setting]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.route :as route-handler]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [goog.dom :as gdom]
+            [rum.core :as rum]))
+
+(def term-chan (chan))
+(def debounce-chan-mult (a/mult (api/debounce term-chan 500)))
+
+(rum/defc zotero-search-item [{:keys [data] :as item} id]
+  (let [[is-creating-page set-is-creating-page!] (rum/use-state false)]
+    (let [title (:title data)
+          type (:item-type data)
+          abstract (str (subs (:abstract-note data) 0 200) "...")]
+
+      [:div.zotero-search-item.px-2.py-4.border-b.cursor-pointer.border-solid.last:border-none.relative
+       {:on-click (fn [] (go
+                           (set-is-creating-page! true)
+                           (<!
+                            (zotero-handler/create-zotero-page item {:block-dom-id id}))
+                           (set-is-creating-page! false)))}
+       [[:div [[:span.font-bold.mb-1.mr-1 title]
+               [:span.zotero-search-item-type.text-xs.p-1.rounded type]]]
+        [:div.text-sm abstract]]
+
+       (when is-creating-page [:div.zotero-search-item-loading-indicator [:span.animate-spin-reverse  svg/refresh]])])))
+
+(rum/defc zotero-search
+  [id]
+
+  (let [[term set-term!]                         (rum/use-state "")
+        [search-result set-search-result!]       (rum/use-state [])
+        [prev-page set-prev-page!]               (rum/use-state "")
+        [next-page set-next-page!]               (rum/use-state "")
+        [prev-search-term set-prev-search-term!] (rum/use-state "")
+        [search-error set-search-error!]         (rum/use-state nil)
+        [is-searching set-is-searching!]         (rum/use-state false)
+
+        search-fn (fn [s-term start]
+                    (go
+                      (when-not (str/blank? s-term)
+                        (set-is-searching! true)
+
+                        (let [{:keys [success next prev result] :as response}
+                              (<! (api/query-top-items s-term start))]
+                          (if (false? success)
+                            (set-search-error! (:body response))
+
+                            (do
+                              (set-prev-search-term! s-term)
+                              (set-next-page! next)
+                              (set-prev-page! prev)
+                              (set-search-result! result))))
+
+                        (set-is-searching! false))))]
+
+    (rum/use-effect!
+     (fn []
+       (let [d-chan (chan)]
+         (a/tap debounce-chan-mult d-chan)
+         (go-loop []
+           (let [d-term (<! d-chan)]
+             (<! (search-fn d-term "0")))
+           (recur))
+
+         (fn [] (a/untap debounce-chan-mult d-chan))))
+     [])
+
+    (when-not (setting/valid?)
+      (route-handler/redirect! {:to :zotero-setting})
+      (notification/show! "Please setup Zotero API key and user/group id first!" :warn false))
+
+    [:div#zotero-search.zotero-search.p-4
+     {:style {:width 600}}
+
+     [:div.flex.items-center.mb-2
+      [[:input.p-2.border.mr-2.flex-1.focus:outline-none
+        {:autoFocus   true
+         :placeholder "Search for your Zotero journal article (title, author, text, anything)"
+         :value       term :on-change (fn [e]
+                                        (go
+                                          (>! term-chan (util/evalue e)))
+                                        (set-term! (util/evalue e)))}]
+
+       [:span.animate-spin-reverse {:style {:visibility (if is-searching "visible"  "hidden")}}  svg/refresh]]]
+
+     [:div.h-2.text-sm.text-red-400.mb-2 (if search-error (str "Search error: " search-error) "")]
+
+     [:div
+      (map
+       (fn [item] (rum/with-key (zotero-search-item item id) (:key item)))
+       search-result)
+      (when-not (str/blank? prev-page)
+        (ui/button
+         "prev"
+         :on-click
+         (fn []
+           (set! (.-scrollTop (.-parentNode (gdom/getElement "zotero-search"))) 0)
+           (go (<! (search-fn prev-search-term prev-page))))))
+      (when-not (str/blank? next-page)
+        (ui/button
+         "next"
+         :on-click
+         (fn []
+           (set! (.-scrollTop (.-parentNode (gdom/getElement "zotero-search"))) 0)
+           (go (<! (search-fn prev-search-term next-page))))))]]))
+
+
+(rum/defcs settings
+  <
+  (rum/local (setting/setting :type-id) ::type-id)
+  (rum/local nil ::progress)
+  (rum/local false ::total)
+  (rum/local "Add all" ::fetching-button)
+  rum/reactive
+  [state]
+  [:div.zotero-settings
+   [:h1.mb-4.text-4xl.font-bold.mb-8 "Zotero Settings"]
+
+   [:div.row
+    [:label.title
+     {:for "zotero_api_key"}
+     "Zotero API key"]
+    [:div.mt-1.sm:mt-0.sm:col-span-2
+     [:div.max-w-lg.rounded-md
+      [:input.form-input.block
+       {:default-value (setting/api-key)
+        :placeholder   "Please enter your Zotero API key"
+        :on-blur       (fn [e] (setting/set-api-key (util/evalue e)))}]]]]
+
+   [:div.row
+    [:label.title
+     {:for "zotero_type"}
+     "Zotero user or group?"]
+    [:div.mt-1.sm:mt-0.sm:col-span-2
+     [:div.max-w-lg.rounded-md
+      [:select.form-select
+       {:value     (-> (setting/setting :type) name)
+        :on-change (fn [e]
+                     (let [type (-> (util/evalue e)
+                                    (str/lower-case)
+                                    keyword)]
+                       (setting/set-setting! :type type)))}
+       (for [type (map name [:user :group])]
+         [:option {:key type :value type} (str/capitalize type)])]]]]
+
+   [:div.row
+    [:label.title
+     {:for "zotero_type_id"}
+     "User or Group ID"]
+    [:div.mt-1.sm:mt-0.sm:col-span-2
+     [:div.max-w-lg.rounded-md
+      [:input.form-input.block
+       {:default-value (setting/setting :type-id)
+        :placeholder   "User/Group id"
+        :on-blur       (fn [e] (setting/set-setting! :type-id (util/evalue e)))
+        :on-change     (fn [e] (reset! (::type-id state) (util/evalue e)))}]]]]
+
+   (when
+    (and (not (str/blank? (str @(::type-id state))))
+         (not (re-matches #"^\d+$" (str @(::type-id state)))))
+     (ui/admonition
+      :warning
+      [:p.text-red-500
+       "User ID is different from username and can be found on the "
+       [:a {:href "https://www.zotero.org/settings/keys" :target "_blank"}
+        "https://www.zotero.org/settings/keys"]
+       " page, it's a number of digits"]))
+
+   [:div.row
+    [:label.title
+     {:for "zotero_include_attachment_links"}
+     "Include attachment links?"]
+    [:div
+     [:div.rounded-md.sm:max-w-xs
+      (ui/toggle (setting/setting :include-attachments?)
+                 (fn [] (setting/set-setting! :include-attachments? (not (setting/setting :include-attachments?))))
+                 true)]]]
+
+   (when (setting/setting :include-attachments?)
+     [:div.row
+      [:label.title
+       {:for "zotero_attachments_block_text"}
+       "Attachtment under block of:"]
+      [:div.mt-1.sm:mt-0.sm:col-span-2
+       [:div.max-w-lg.rounded-md
+        [:input.form-input.block
+         {:default-value (setting/setting :attachments-block-text)
+          :on-blur       (fn [e] (setting/set-setting! :attachments-block-text (util/evalue e)))}]]]])
+
+   [:div.row
+    [:label.title
+     {:for "zotero_include_notes"}
+     "Include notes?"]
+    [:div
+     [:div.rounded-md.sm:max-w-xs
+      (ui/toggle (setting/setting :include-notes?)
+                 (fn [] (setting/set-setting! :include-notes?
+                                              (not (setting/setting :include-notes?))))
+                 true)]]]
+
+   (when (setting/setting :include-notes?)
+     [:div.row
+      [:label.title
+       {:for "zotero_notes_block_text"}
+       "Notes under block of:"]
+      [:div.mt-1.sm:mt-0.sm:col-span-2
+       [:div.max-w-lg.rounded-md
+        [:input.form-input.block
+         {:default-value (setting/setting :notes-block-text)
+          :on-blur       (fn [e] (setting/set-setting! :notes-block-text (util/evalue e)))}]]]])
+
+   [:div.row
+    [:label.title
+     {:for "zotero_page_prefix"}
+     "Insert page name with prefix:"]
+    [:div.mt-1.sm:mt-0.sm:col-span-2
+     [:div.max-w-lg.rounded-md
+      [:input.form-input.block
+       {:default-value (setting/setting :page-insert-prefix)
+        :on-blur       (fn [e] (setting/set-setting! :page-insert-prefix (util/evalue e)))}]]]]
+
+   [:div.row
+    [:label.title
+     {:for "zotero_import_all"}
+     "Add all zotero items"]
+    [:div.mt-1.sm:mt-0.sm:col-span-2
+     (ui/button
+      @(::fetching-button state)
+      :on-click
+      (fn []
+        (go
+          (let [_     (reset! (::fetching-button state) "Fetching..")
+                total (<! (api/all-top-items-count))
+                _     (reset! (::fetching-button state) "Add all")]
+            (when (.confirm
+                   js/window
+                   (str "This will import all your zotero items and add total number of " total " pages. Do you wish to continue?"))
+              (do
+                (reset! (::total state) total)
+                (<! (zotero-handler/add-all (::progress state)))
+                (reset! (::total state) false)
+                (notification/show! "Successfully added all items!" :success)))))))]]
+
+   (when @(::total state)
+     [:div.row
+      [:div.bg-greenred-200.py-3.rounded-lg.col-span-full
+       [:progress.w-full {:max (+ @(::total state) 30) :value @(::progress state)}] "Importing items from Zotero....Please wait..."]])])

+ 36 - 0
src/main/frontend/extensions/zotero.css

@@ -0,0 +1,36 @@
+.zotero-settings {
+  .row {
+    @apply sm:grid sm:grid-cols-3 sm:gap-4 mb-4 flex items-center;
+  }
+
+  .title {
+    @apply block font-medium leading-5;
+  }
+
+  .form-select {
+    @apply py-2;
+  }
+}
+
+.zotero-search-item-loading-indicator {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  text-align: center;
+  font-size: 60px;
+  color: black;
+  background-color: rgba(255, 255, 255, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.zotero-search-item:hover {
+  background-color: var(--ls-secondary-background-color);
+}
+
+.zotero-search-item-type {
+  background-color: var(--ls-quaternary-background-color);
+}

+ 156 - 0
src/main/frontend/extensions/zotero/api.cljs

@@ -0,0 +1,156 @@
+(ns frontend.extensions.zotero.api
+  (:require [camel-snake-kebab.core :as csk]
+            [camel-snake-kebab.extras :as cske]
+            [cljs-http.client :as http]
+            [cljs.core.async
+             :refer [<! >! alt! chan close! go go-loop timeout]]
+            [clojure.string :as str]
+            [frontend.util :as util]
+            [frontend.extensions.zotero.setting :as setting]))
+
+(defn config []
+  {:api-version 3
+   :base        "https://api.zotero.org"
+   :timeout     150000
+   :api-key     (setting/api-key)
+   :type        (setting/setting :type)
+   :type-id     (setting/setting :type-id)})
+
+;; taken from https://github.com/metosin/metosin-common/blob/master/src/cljc/metosin/core/async/debounce.cljc
+(defn debounce
+  "Creates a channel which will change put a new value to the output channel
+   after timeout has passed. Each value change resets the timeout. If value
+   changes more frequently only the latest value is put out.
+   When input channel closes, the output channel is closed."
+  [in ms]
+  (let [out (chan)]
+    (go-loop [last-val nil]
+      (let [val   (if (nil? last-val) (<! in) last-val)
+            timer (timeout ms)]
+        (alt!
+          in ([v] (if v
+                    (recur v)
+                    (close! out)))
+          timer ([_] (do (>! out val) (recur nil))))))
+    out))
+
+(defn parse-start [headers next-or-prev]
+  (let [inclue-text (case next-or-prev
+                      :next "rel=\"next\""
+                      :prev "rel=\"prev\"")
+        links
+        (str/split
+         (:link (cske/transform-keys csk/->kebab-case-keyword headers)) ",")
+        next-link   (->> links
+                       (filter (fn [l] (str/includes? l inclue-text)))
+                       first)]
+    (when next-link
+      (let [start    (str/index-of next-link "<")
+            end      (str/last-index-of next-link ">;")
+            next-url (subs next-link (inc start) end)]
+        (or
+         (->
+          next-url
+          http/parse-url
+          :query-params
+          :start)
+         "0")))))
+
+(defn results-count [headers]
+  (-> (cske/transform-keys csk/->kebab-case-keyword headers)
+      :total-results
+      util/safe-parse-int))
+
+;; "/users/475425/collections?v=3"
+(defn get*
+  ([config api]
+   (get* config api nil))
+  ([config api query-params]
+   (go (let [{:keys [api-version base type type-id api-key timeout]} config
+             {:keys [success body headers] :as response}
+             (<! (http/get (str base
+                                (if (= type :user)
+                                  "/users/"
+                                  "/groups/")
+                                type-id
+                                api)
+                           {:timeout           timeout
+                            :with-credentials? false
+                            :headers           {"Zotero-API-Key"     api-key
+                                                "Zotero-API-Version" api-version}
+                            :query-params      (cske/transform-keys csk/->camelCaseString
+                                                                    query-params)}))]
+         (if success
+           (let [result     (cske/transform-keys csk/->kebab-case-keyword body)
+                 next-start (parse-start headers :next)
+                 prev-start (parse-start headers :prev)
+                 results-count (results-count headers)]
+             (cond-> {:result result}
+               next-start
+               (assoc :next next-start)
+               prev-start
+               (assoc :prev prev-start)
+               results-count
+               (assoc :count results-count)))
+           response)))))
+
+(defn item [key]
+  (:result (get* (config) (str "/items/" key))))
+
+(defn all-top-items-count []
+  (go
+    (:count
+     (<! (get* (config) (str "/items/top")
+               {:limit     1
+                :item-type "-attachment"})))))
+
+(defn all-top-items []
+  (go-loop [start "0"
+            result-acc []]
+    (let [{:keys [success next result]}
+          (<! (get* (config) (str "/items/top")
+                    {:item-type "-attachment"
+                     :start     start}))]
+      (cond
+        (false? success)
+        result-acc
+
+        next
+        (recur next (into [] (concat result-acc result)))
+
+        :else
+        (into [] (concat result-acc result))))))
+
+(defn query-top-items
+  "Query all top level items except attachments"
+  ([term]
+   (query-top-items term "0"))
+  ([term start]
+   (get* (config) (str "/items/top")
+         {:qmode     "everything"
+          :q         term
+          :limit     10
+          :item-type "-attachment"
+          :start     start})))
+
+(defn all-children-items [key type]
+  (go-loop [start "0"
+            notes-acc []]
+    (let [{:keys [success next result]}
+          (<! (get* (config) (str "/items/" key "/children")
+                    {:item-type type :start start}))]
+      (cond
+        (false? success)
+        notes-acc
+
+        next
+        (recur next (into [] (concat notes-acc result)))
+
+        :else
+        (into [] (concat notes-acc result))))))
+
+(defn notes [key]
+  (all-children-items key "note"))
+
+(defn attachments [key]
+  (all-children-items key "attachment"))

+ 158 - 0
src/main/frontend/extensions/zotero/extractor.cljs

@@ -0,0 +1,158 @@
+(ns frontend.extensions.zotero.extractor
+  (:require [clojure.string :as str]
+            [frontend.util :as util]
+            [frontend.extensions.zotero.schema :as schema]
+            [frontend.extensions.html-parser :as html-parser]
+            [frontend.date :as date]
+            [clojure.string :as string]
+            [clojure.set :refer [rename-keys]]
+            [frontend.extensions.zotero.setting :as setting]
+            [frontend.extensions.zotero.api :as api]))
+
+(defn item-type [item] (-> item :data :item-type))
+
+(defmulti extract item-type)
+
+(defn citation-key [item]
+  (let [extra (-> item :data :extra)
+        citation (->> extra
+                      (str/split-lines)
+                      (filterv (fn [s] (str/includes? s "Citation Key: ")))
+                      first)]
+    (when citation
+      (str/trim (str/replace citation "Citation Key: " "")))))
+
+(defn title [item] (-> item :data :title))
+
+(defn item-key [item] (:key item))
+
+(defn page-name [item]
+  (let [page-title
+        (case (item-type item)
+          "journalArticle"
+          (let [citation-key (citation-key item)
+                title        (title item)]
+            (or citation-key title))
+          "case"
+          (-> item :data :case-name)
+          "email"
+          (-> item :data :subject)
+          "statute"
+          (-> item :data :name-of-act)
+          ;; default use title
+          (title item))]
+    (str (setting/setting :page-insert-prefix) page-title "_" (item-key item))))
+
+(defn authors [item]
+  (let [creators (-> item :data :creators)
+        authors
+        (into []
+              (comp
+               (filter (fn [m] (= "author" (:creator-type m))))
+               (map (fn [{:keys [first-name last-name name]}]
+                      (string/trim (if name name (str first-name " " last-name))))))
+              creators)]
+    (distinct authors)))
+
+(defn tags [item]
+  (let [tags
+        (->> (-> item :data :tags)
+             (mapv (fn [{:keys [tag]}] (string/trim tag)))
+             (mapcat #(string/split % #",\s?")))]
+    (distinct tags)))
+
+(defn date->journal [item]
+  (if-let [date (-> item :meta :parsed-date
+                      (date/journal-name-s))]
+    (util/format "[[%s]]" date)
+    (-> item :data :date)))
+
+(defn wrap-in-doublequotes [m]
+  (->> m
+       (map (fn [[k v]]
+              (if (str/includes? (str v) ",")
+                [k (pr-str v)]
+                [k v])))
+       (into (array-map))))
+
+(defn skip-newline-properties [m]
+  (->> m
+       (remove (fn [[_ v]] (str/includes? (str v) "\n")))
+       (into (array-map))))
+
+(defn markdown-link [label link]
+  (util/format "[%s](%s)" label link))
+
+(defn local-link [item]
+  (let [type (-> item :library :type)
+        id   (-> item :library :id)
+        library
+        (if (= type "user")
+          "library"
+          (str "groups/" id))
+        item-key (-> item :key)]
+    (util/format "zotero://select/%s/items/%s" library item-key)))
+
+(defn web-link [item]
+  (let [type (-> item :library :type)
+        id   (-> item :library :id)
+        library
+        (if (= type "user")
+          (str "users/" id)
+          (str "groups/" id))
+        item-key (-> item :key)]
+    (util/format "https://www.zotero.org/%s/items/%s" library item-key)))
+
+(defn zotero-links [item]
+  (str (markdown-link "Local library" (local-link item))
+       ", "
+       (markdown-link "Web library" (web-link item))))
+
+(defn properties [item]
+  (let [type    (item-type item)
+        fields  (schema/fields type)
+        authors (authors item)
+        tags    (tags item)
+        links   (zotero-links item)
+        date    (date->journal item)
+        data    (-> item :data
+                         (select-keys fields)
+                         (skip-newline-properties)
+                         (wrap-in-doublequotes)
+                         (assoc :links links
+                                :authors authors
+                                :tags tags
+                                :date date
+                                :item-type (util/format "[[%s]]" type))
+                         (dissoc :creators)
+                         (rename-keys {:title :original-title})
+                         (assoc :title (page-name item)))]
+    (->> data
+         (remove (comp (fn [v] (or (str/blank? v) (empty? v))) second))
+         (into {}))))
+
+(defmethod extract "note"
+  [item]
+  (let [note-html (-> item :data :note)]
+    (html-parser/parse :markdown note-html)))
+
+(defmethod extract "attachment"
+  [item]
+  (let [{:keys [title url link-mode path]} (-> item :data)]
+    (case link-mode
+      "imported_file"
+      (markdown-link title (local-link item))
+      "imported_url"
+      (markdown-link title url)
+      "linked_file"
+      (markdown-link title (str "file://" path))
+      "linked_url"
+      (markdown-link title url)
+      nil)))
+
+(defmethod extract :default
+  [item]
+  (let [page-name  (page-name item)
+        properties (properties item)]
+    {:page-name  page-name
+     :properties properties}))

+ 84 - 0
src/main/frontend/extensions/zotero/handler.cljs

@@ -0,0 +1,84 @@
+(ns frontend.extensions.zotero.handler
+  (:require [cljs.core.async :refer [<! go]]
+            [clojure.string :as str]
+            [frontend.extensions.zotero.api :as zotero-api]
+            [frontend.extensions.zotero.extractor :as extractor]
+            [frontend.extensions.zotero.setting :as setting]
+            [frontend.handler.notification :as notification]
+            [frontend.state :as state]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.handler.page :as page-handler]))
+
+(defn add [page-name type item]
+  (go
+    (let [key         (:key item)
+          num-children (-> item :meta :num-children)
+          api-fn      (case type
+                        :notes       zotero-api/notes
+                        :attachments zotero-api/attachments)
+          first-block (case type
+                        :notes       (setting/setting :notes-block-text)
+                        :attachments (setting/setting :attachments-block-text))
+          should-add? (case type
+                        :notes       (setting/setting :include-notes?)
+                        :attachments (setting/setting :include-attachments?))]
+      (when (and should-add? (> num-children 0))
+        (let [items    (<! (api-fn key))
+              md-items (->> items
+                            (map extractor/extract)
+                            (remove str/blank?))]
+          (when (not-empty md-items)
+            (when-let [id (:block/uuid
+                           (editor-handler/api-insert-new-block!
+                            first-block {:page page-name}))]
+              (doseq [md-item md-items]
+                (editor-handler/api-insert-new-block!
+                 md-item
+                 {:block-uuid id
+                  :sibling?   false
+                  :before?    false})))))))))
+
+(defn handle-command-zotero
+  [id page-name]
+  (state/set-editor-show-zotero! false)
+  (editor-handler/insert-command! id (str "[[" page-name "]]") nil {}))
+
+(defn create-zotero-page
+  ([item]
+   (create-zotero-page item {}))
+  ([item {:keys [block-dom-id insert-command? notification?]
+          :or {insert-command? true notification? true}
+          :as opt}]
+   (go
+     (let [{:keys [page-name properties]} (extractor/extract item)]
+
+       (when insert-command?
+         (handle-command-zotero block-dom-id page-name)
+         (editor-handler/save-current-block!))
+
+       (if (page-handler/page-exists? (str/lower-case page-name))
+         (editor-handler/api-insert-new-block!
+          ""
+          {:page       page-name
+           :properties properties})
+         (page-handler/create!
+          page-name
+          {:redirect? false
+           :format :markdown
+           :create-first-block? false
+           :properties properties}))
+
+       (<! (add page-name :attachments item))
+
+       (<! (add page-name :notes item))
+
+       (when notification?
+         (notification/show! (str "Successfully added zotero item to page " page-name) :success))))))
+
+(defn add-all [progress]
+  (go
+    (let [all-items (<! (zotero-api/all-top-items))]
+      (reset! progress 30)
+      (doseq [item all-items]
+        (<! (create-zotero-page item {:insert-command? false :notification? false}))
+        (swap! progress inc)))))

+ 15 - 0
src/main/frontend/extensions/zotero/schema.cljs

@@ -0,0 +1,15 @@
+(ns frontend.extensions.zotero.schema
+  (:require [camel-snake-kebab.core :as csk]
+            [clojure.edn :as edn]
+            [shadow.resource :as rc]))
+
+(def items-with-fields
+  (-> (rc/inline "zotero-items.edn")
+      (edn/read-string)))
+
+(defn fields [type]
+  (->> items-with-fields
+       (filter (fn [{:keys [item-type]}] (= item-type type)))
+       (first)
+       :fields
+       (mapv csk/->kebab-case-keyword)))

+ 37 - 0
src/main/frontend/extensions/zotero/setting.cljs

@@ -0,0 +1,37 @@
+(ns frontend.extensions.zotero.setting
+  (:require [clojure.string :as str]
+            [frontend.handler.config :as config-handler]
+            [frontend.state :as state]
+            [frontend.storage :as storage]))
+
+(def default-settings
+  {:type                   :user
+   :include-attachments?   true
+   :attachments-block-text "[[attachments]]"
+   :include-notes?         true
+   :notes-block-text       "[[notes]]"
+   :page-insert-prefix     "@"})
+
+(defn api-key []
+  (storage/get :zotero/api-key))
+
+(defn set-api-key [key]
+  (storage/set :zotero/api-key key))
+
+(defn sub-zotero-config
+  []
+  (:zotero/settings (get (state/sub-config) (state/get-current-repo))))
+
+(defn set-setting! [k v]
+  (let [new-settings (assoc (sub-zotero-config) k v)]
+    (config-handler/set-config! :zotero/settings new-settings)))
+
+(defn setting [k]
+  (get (sub-zotero-config)
+       k
+       (get default-settings k)))
+
+(defn valid? []
+  (and
+   (not (str/blank? (api-key)))
+   (not (str/blank? (setting :type-id)))))

+ 18 - 2
src/main/frontend/format/block.cljs

@@ -199,7 +199,8 @@
           page-refs (->>
                      (map (fn [v]
                             (when (string? v)
-                              (let [result (text/split-page-refs-without-brackets v {:un-brackets? false})]
+                              (let [v (string/trim v)
+                                    result (text/split-page-refs-without-brackets v {:un-brackets? false})]
                                 (if (coll? result)
                                   (map text/page-ref-un-brackets! result)
                                   []))))
@@ -215,7 +216,7 @@
                                            "id"
                                            k)
                                        v (if (coll? v)
-                                           v
+                                           (remove util/wrapped-by-quotes? v)
                                            (property/parse-property k v))
                                        k (keyword k)
                                        v (if (and
@@ -464,6 +465,19 @@
             (uuid custom-id))))
       (db/new-block-id)))
 
+(defn get-page-refs-from-properties
+  [properties]
+  (let [page-refs (mapcat (fn [v] (cond
+                                   (coll? v)
+                                   v
+
+                                   (text/page-ref? v)
+                                   [(text/page-ref-un-brackets! v)]
+
+                                   :else
+                                   nil)) (vals properties))]
+    (map (fn [page] (page-name->map page true)) page-refs)))
+
 (defn extract-blocks
   [blocks content with-id? format]
   (try
@@ -593,6 +607,8 @@
                                                (utf8/length encoded-content))}
                            :body @pre-block-body
                            :properties @pre-block-properties
+                           :properties-order (keys @pre-block-properties)
+                           :refs (get-page-refs-from-properties @pre-block-properties)
                            :pre-block? true
                            :unordered true}
                           (block-keywordize)))

+ 6 - 1
src/main/frontend/format/mldoc.cljs

@@ -184,7 +184,8 @@
                          (update :roam_alias ->vec)
                          (update :roam_tags (constantly roam-tags))
                          (update :filetags (constantly filetags)))
-          properties (medley/filter-kv (fn [k v] (not (empty? v))) properties)]
+          properties (medley/filter-kv (fn [k v] (not (empty? v))) properties)
+          properties (medley/map-vals util/unquote-string-if-wrapped properties)]
       (if (seq properties)
         (cons [["Properties" properties] nil] other-ast)
         original-ast))
@@ -266,3 +267,7 @@
 (defn plain->text
   [plains]
   (string/join (map last plains)))
+
+(defn properties?
+  [ast]
+  (contains? #{"Properties" "Property_Drawer"} (ffirst ast)))

+ 55 - 29
src/main/frontend/handler/editor.cljs

@@ -266,9 +266,10 @@
                     (util/distinct-by :block/name))
           {:keys [tags alias]} page-properties
           page-tx (let [id (:db/id (:block/page block))
-                        retract-attributes (mapv (fn [attribute]
-                                                   [:db/retract id attribute])
-                                                 [:block/properties :block/tags :block/alias])
+                        retract-attributes (when id
+                                             (mapv (fn [attribute]
+                                                     [:db/retract id attribute])
+                                                   [:block/properties :block/tags :block/alias]))
                         tx (cond-> {:db/id id
                                     :block/properties page-properties}
                              (seq tags)
@@ -539,6 +540,7 @@
 (defn clear-when-saved!
   []
   (state/set-editor-show-input! nil)
+  (state/set-editor-show-zotero! false)
   (state/set-editor-show-date-picker! false)
   (state/set-editor-show-page-search! false)
   (state/set-editor-show-block-search! false)
@@ -647,12 +649,14 @@
               new-block (-> (select-keys block [:block/page :block/file :block/journal?
                                                 :block/journal-day])
                             (assoc :block/content content
-                                   :block/format format)
+                                   :block/format format))
+              new-block (assoc new-block :block/page
+                               (if page
+                                 (:db/id block)
+                                 (:db/id (:block/page new-block))))
+              new-block (-> new-block
                             (wrap-parse-block)
-                            (assoc :block/uuid (or custom-uuid (db/new-block-id))))
-              new-block (if (:block/page new-block)
-                          (assoc new-block :block/page (:db/id (:block/page new-block)))
-                          (assoc new-block :block/page (:db/id block)))
+                            (assoc :block/uuid (db/new-block-id)))
               new-block (if-let [db-id (:db/id (:block/file block))]
                           (assoc new-block :block/file db-id)
                           new-block)]
@@ -686,13 +690,15 @@
       (when (db/page-empty? (state/get-current-repo) (:db/id page))
         (api-insert-new-block! "" {:page page-name})))))
 
-(defn default-properties-block
-  [title format page]
-  (let [properties (common-handler/get-page-default-properties title)
-        content (property/build-properties-str format properties)]
+(defn properties-block
+  [properties format page]
+  (let [content (property/insert-properties format "" properties)
+        refs (block/get-page-refs-from-properties properties)]
     {:block/pre-block? true
      :block/uuid (db/new-block-id)
      :block/properties properties
+     :block/properties-order (keys properties)
+     :block/refs refs
      :block/left page
      :block/format format
      :block/content content
@@ -700,6 +706,26 @@
      :block/unordered true
      :block/page page}))
 
+(defn default-properties-block
+  ([title format page]
+   (default-properties-block title format page {}))
+  ([title format page properties]
+   (let [p (common-handler/get-page-default-properties title)
+         ps (merge p properties)
+         content (property/insert-properties format "" ps)
+         refs (block/get-page-refs-from-properties properties)]
+     {:block/pre-block? true
+      :block/uuid (db/new-block-id)
+      :block/properties ps
+      :block/properties-order (keys ps)
+      :block/refs refs
+      :block/left page
+      :block/format format
+      :block/content content
+      :block/parent page
+      :block/unordered true
+      :block/page page})))
+
 (defn add-default-title-property-if-needed!
   [page-name]
   (when (string? page-name)
@@ -956,7 +982,7 @@
                                                (if (string/starts-with? (string/lower-case line) key)
                                                  new-line
                                                  line))
-                                          lines)
+                                             lines)
                               new-lines (if (not= lines new-lines)
                                           new-lines
                                           (cons (first new-lines) ;; title
@@ -1038,8 +1064,8 @@
   (let [blocks (db-utils/pull-many repo '[*] (mapv (fn [id] [:block/uuid id]) block-ids))
         blocks* (flatten
                  (mapv (fn [b] (if (:collapsed (:block/properties b))
-                                (vec (tree/sort-blocks (db/get-block-children repo (:block/uuid b)) b))
-                                [b])) blocks))
+                                 (vec (tree/sort-blocks (db/get-block-children repo (:block/uuid b)) b))
+                                 [b])) blocks))
         block-ids* (mapv :block/uuid blocks*)
         unordered? (:block/unordered (first blocks*))
         format (:block/format (first blocks*))
@@ -1712,7 +1738,7 @@
         (seq blocks)
         (do
           (let [lookup-refs (->> (map (fn [block] (when-let [id (dom/attr block "blockid")]
-                                                   [:block/uuid (medley/uuid id)])) blocks)
+                                                    [:block/uuid (medley/uuid id)])) blocks)
                                  (remove nil?))
                 blocks (db/pull-many repo '[*] lookup-refs)
                 blocks (reorder-blocks blocks)
@@ -1727,12 +1753,12 @@
               (db/refresh! repo opts)
               (let [blocks (doall
                             (map
-                              (fn [block]
-                                (when-let [id (gobj/get block "id")]
-                                  (when-let [block (gdom/getElement id)]
-                                    (dom/add-class! block "selected noselect")
-                                    block)))
-                              blocks-dom-nodes))]
+                             (fn [block]
+                               (when-let [id (gobj/get block "id")]
+                                 (when-let [block (gdom/getElement id)]
+                                   (dom/add-class! block "selected noselect")
+                                   block)))
+                             blocks-dom-nodes))]
                 (state/set-selection-blocks! blocks)))))))))
 
 (defn- get-link
@@ -2093,11 +2119,11 @@
           tree (blocks-vec->tree result-blocks)]
       (insert-command! id "" format {})
       (let [last-block (paste-block-vec-tree-at-target tree [:template :template-including-parent]
-                                                  (fn [content]
-                                                    (->> content
-                                                         (property/remove-property format "template")
-                                                         (property/remove-property format "template-including-parent")
-                                                         template/resolve-dynamic-template!)))]
+                                                       (fn [content]
+                                                         (->> content
+                                                              (property/remove-property format "template")
+                                                              (property/remove-property format "template-including-parent")
+                                                              template/resolve-dynamic-template!)))]
         (clear-when-saved!)
         (db/refresh! repo {:key :block/insert :data [(db/pull db-id)]})
         ;; FIXME:
@@ -2624,8 +2650,8 @@
   (let [min-level (apply min (mapv :block/level blocks))
         prefix-level (if (> min-level 1) (- min-level 1) 0)]
     (->> blocks
-                   (mapv #(assoc % :level (- (:block/level %) prefix-level)))
-                   (blocks-vec->tree))))
+         (mapv #(assoc % :level (- (:block/level %) prefix-level)))
+         (blocks-vec->tree))))
 
 (defn- paste-text-parseable
   [format text]

+ 2 - 1
src/main/frontend/handler/editor/lifecycle.cljs

@@ -17,7 +17,8 @@
         content (get-in @state/state [:editor/content id])]
     (when block-parent-id
       (state/set-editing-block-dom-id! block-parent-id))
-    (editor-handler/restore-cursor-pos! id content)
+    (when content
+      (editor-handler/restore-cursor-pos! id content))
 
     ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event,
     ;; which will hide the editor so no way for editing.

+ 17 - 15
src/main/frontend/handler/extract.cljs

@@ -190,20 +190,20 @@
          (with-block-uuid))))
 
 (defn extract-all-blocks-pages
-  [repo-url files metadata]
+  [repo-url files metadata refresh?]
   (when (seq files)
     (let [result (->> files
                       (map
-                       (fn [{:file/keys [path content]} contents]
-                         (println "Parsing : " path)
-                         (when content
-                           ;; TODO: remove `text/scheduled-deadline-dash->star` once migration is done
-                           (let [org? (= "org" (string/lower-case (util/get-file-ext path)))]
-                             (let [content (if org?
-                                             content
-                                             (text/scheduled-deadline-dash->star content))
-                                   utf8-content (utf8/encode content)]
-                              (extract-blocks-pages repo-url path content utf8-content))))))
+                        (fn [{:file/keys [path content]} contents]
+                          (println "Parsing : " path)
+                          (when content
+                            ;; TODO: remove `text/scheduled-deadline-dash->star` once migration is done
+                            (let [org? (= "org" (string/lower-case (util/get-file-ext path)))]
+                              (let [content (if org?
+                                              content
+                                              (text/scheduled-deadline-dash->star content))
+                                    utf8-content (utf8/encode content)]
+                                (extract-blocks-pages repo-url path content utf8-content))))))
                       (remove empty?))]
       (when (seq result)
         (let [[pages block-ids blocks] (apply map concat result)
@@ -212,14 +212,16 @@
                             (let [id (:block/uuid block)
                                   properties (get-in metadata [:block/properties id])]
                               (update block :block/properties merge properties)))
-                          blocks)
+                       blocks)
               ;; To prevent "unique constraint" on datascript
               pages-index (map #(select-keys % [:block/name]) pages)
               block-ids-set (set (map (fn [{:block/keys [uuid]}] [:block/uuid uuid]) block-ids))
               blocks (map (fn [b]
                             (update b :block/refs
                                     (fn [refs]
-                                      (set/union
-                                       (filter :block/name refs)
-                                       (set/intersection (set refs) block-ids-set))))) blocks)]
+                                      (let [block-refs (if refresh? (set refs)
+                                                           (set/intersection (set refs) block-ids-set))]
+                                        (set/union
+                                         (filter :block/name refs)
+                                         block-refs))))) blocks)]
           (apply concat [pages-index pages block-ids blocks]))))))

+ 63 - 25
src/main/frontend/handler/page.cljs

@@ -20,6 +20,8 @@
             [frontend.modules.outliner.tree :as outliner-tree]
             [frontend.commands :as commands]
             [frontend.date :as date]
+            [frontend.db-schema :as db-schema]
+            [frontend.db.model :as model]
             [clojure.walk :as walk]
             [frontend.git :as git]
             [frontend.fs :as fs]
@@ -50,37 +52,63 @@
   ([page-name] (when-let [page (db/entity [:block/name page-name])]
                  (:file/path (:block/file page)))))
 
+(defn- build-title [page]
+  (let [original-name (:block/original-name page)]
+    (if (string/includes? original-name ",")
+      (util/format "\"%s\"" original-name)
+      original-name)))
+
+(defn- build-page-tx [format properties page]
+  (when (:block/uuid page)
+    (let [page-entity [:block/uuid (:block/uuid page)]
+          create-title-property? (util/create-title-property? (:block/name page))]
+      (cond
+        (and properties create-title-property?)
+        [page (editor-handler/default-properties-block (build-title page) format page-entity properties)]
+
+        create-title-property?
+        [page (editor-handler/default-properties-block (build-title page) format page-entity)]
+
+        properties
+        [page (editor-handler/properties-block properties format page-entity)]
+
+        :else
+        [page]))))
+
 (defn create!
   ([title]
    (create! title {}))
-  ([title {:keys [redirect? create-first-block?]
-           :or {redirect? true
-                create-first-block? true}}]
-   (let [title (string/trim title)
-         pages (util/split-namespace-pages title)
-         page (string/lower-case title)
-         format (state/get-preferred-format)
-         pages (map (fn [page]
-                      (-> (block/page-name->map page true)
-                          (assoc :block/format format)))
-                 pages)
-         txs (->>
-              (mapcat
-               (fn [page]
-                 (when (:block/uuid page)
-                   (let [page-entity [:block/uuid (:block/uuid page)]
-                         create-title-property? (util/create-title-property? (:block/name page))]
-                     (if create-title-property?
-                       (let [default-properties (editor-handler/default-properties-block (:block/original-name page) format page-entity)]
-                         [page default-properties])
-                       [page]))))
-               pages)
-              (remove nil?))]
+  ([title {:keys [redirect? create-first-block? format properties]
+           :or   {redirect?           true
+                  create-first-block? true
+                  format              false
+                  properties          false}}]
+   (let [title    (string/trim title)
+         pages    (util/split-namespace-pages title)
+         page     (string/lower-case title)
+         format   (or format (state/get-preferred-format))
+         pages    (map (fn [page]
+                         (-> (block/page-name->map page true)
+                             (assoc :block/format format)))
+                       pages)
+         txs      (->> pages
+                       ;; for namespace pages, only last page need properties
+                       drop-last
+                       (mapcat #(build-page-tx format false %))
+                       (remove nil?))
+         last-txs (build-page-tx format properties (last pages))
+         txs      (concat txs last-txs)]
+
      (db/transact! txs)
+
      (when create-first-block?
        (editor-handler/insert-first-page-block-if-not-exists! page))
+
+     (when-let [page (db/entity [:block/name page])]
+       (outliner-file/sync-to-file page))
+
      (when redirect?
-       (route-handler/redirect! {:to :page
+       (route-handler/redirect! {:to          :page
                                  :path-params {:name page}})))))
 
 (defn page-add-property!
@@ -179,7 +207,16 @@
                (p/catch (fn [err]
                           (js/console.error "error: " err))))))
 
-          (db/transact! [[:db.fn/retractEntity [:block/name page-name]]])
+
+          ;; if other page alias this pagename,
+          ;; then just remove some attrs of this entity instead of retractEntity
+          (if (model/get-alias-source-page (state/get-current-repo) page-name)
+            (when-let [id (:db/id (db/entity [:block/name page-name]))]
+              (let [txs (mapv (fn [attribute]
+                                [:db/retract id attribute])
+                              db-schema/retract-page-attributes)]
+                (db/transact! txs)))
+            (db/transact! [[:db.fn/retractEntity [:block/name page-name]]]))
 
           (ok-handler))))))
 
@@ -368,6 +405,7 @@
 
                   ;; Redirect to the new page
                   (route-handler/redirect! {:to :page
+                                            :push false
                                             :path-params {:name (string/lower-case new-name)}})
 
                   (notification/show! "Page renamed successfully!" :success)

+ 19 - 16
src/main/frontend/handler/repo.cljs

@@ -179,12 +179,12 @@
              item)) data)))
 
 (defn- reset-contents-and-blocks!
-  [repo-url files blocks-pages delete-files delete-blocks]
+  [repo-url files blocks-pages delete-files delete-blocks refresh?]
   (db/transact-files-db! repo-url files)
   (let [files (map #(select-keys % [:file/path :file/last-modified-at]) files)
         all-data (-> (concat delete-files delete-blocks files blocks-pages)
-                     (util/remove-nils)
-                     (remove-non-exists-refs!))]
+                     (util/remove-nils))
+        all-data (if refresh? all-data (remove-non-exists-refs! all-data))]
     (db/transact! repo-url all-data)))
 
 (defn- load-pages-metadata!
@@ -213,21 +213,22 @@
       (log/error :exception e))))
 
 (defn- parse-files-and-create-default-files-inner!
-  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata]
-  (let [parsed-files (filter
+  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts]
+  (let [refresh? (:refresh? opts)
+        parsed-files (filter
                       (fn [file]
                         (let [format (format/get-format (:file/path file))]
                           (contains? config/mldoc-support-formats format)))
                       files)
         blocks-pages (if (seq parsed-files)
-                       (extract-handler/extract-all-blocks-pages repo-url parsed-files metadata)
+                       (extract-handler/extract-all-blocks-pages repo-url parsed-files metadata refresh?)
                        [])]
     (let [config-file (config/get-config-path)]
       (when (contains? (set file-paths) config-file)
         (when-let [content (some #(when (= (:file/path %) config-file)
                                     (:file/content %)) files)]
           (file-handler/restore-config! repo-url content true))))
-    (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
+    (reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks refresh?)
     (load-pages-metadata! repo-url file-paths files)
     (when first-clone?
       (if (and (not db-encrypted?) (state/enable-encryption? repo-url))
@@ -240,15 +241,15 @@
     (state/pub-event! [:graph/added repo-url])))
 
 (defn- parse-files-and-create-default-files!
-  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata]
+  [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts]
   (if db-encrypted?
     (p/let [files (p/all
                    (map (fn [file]
                           (p/let [content (encrypt/decrypt (:file/content file))]
                             (assoc file :file/content content)))
                         files))]
-      (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata))
-    (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)))
+      (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts))
+    (parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts)))
 
 (defn parse-files-and-load-to-db!
   [repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts
@@ -264,14 +265,14 @@
           db-encrypted? (:db/encrypted? metadata)
           db-encrypted-secret (if db-encrypted? (:db/encrypted-secret metadata) nil)]
       (if db-encrypted?
-        (let [close-fn #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)]
+        (let [close-fn #(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts)]
           (state/pub-event! [:modal/encryption-input-secret-dialog repo-url
                              db-encrypted-secret
                              close-fn]))
-        (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata)))))
+        (parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts)))))
 
 (defn load-repo-to-db!
-  [repo-url {:keys [first-clone? diffs nfs-files]
+  [repo-url {:keys [first-clone? diffs nfs-files refresh?]
              :as opts}]
   (spec/validate :repos/url repo-url)
   (let [config (or (state/get-config repo-url)
@@ -288,7 +289,7 @@
                          repo-url
                          files
                          (fn [files-contents]
-                           (parse-files-and-load-to-db! repo-url files-contents option))))]
+                           (parse-files-and-load-to-db! repo-url files-contents (assoc option :refresh? refresh?)))))]
     (cond
       (and (not (seq diffs)) nfs-files)
       (parse-files-and-load-to-db! repo-url nfs-files {:first-clone? true})
@@ -328,10 +329,12 @@
               options {:first-clone? first-clone?
                        :delete-files (concat delete-files delete-pages)
                        :delete-blocks delete-blocks
-                       :re-render? true}]
+                       :re-render? true
+                       :refresh? true}]
           (if (seq nfs-files)
             (parse-files-and-load-to-db! repo-url nfs-files
-                                         (assoc options :re-render-opts {:clear-all-query-state? true}))
+                                         (assoc options
+                                                :re-render-opts {:clear-all-query-state? true}))
             (load-contents add-or-modify-files options)))))))
 
 (defn load-db-and-journals!

+ 2 - 1
src/main/frontend/handler/web/nfs.cljs

@@ -247,7 +247,8 @@
                               (seq diffs))
                       (repo-handler/load-repo-to-db! repo
                                                      {:diffs     diffs
-                                                      :nfs-files modified-files}))))))))
+                                                      :nfs-files modified-files
+                                                      :refresh? true}))))))))
 
 (defn- reload-dir!
   ([repo]

+ 1 - 3
src/main/frontend/modules/file/core.cljs

@@ -29,9 +29,7 @@
         markdown? (= format :markdown)
         content (cond
                   (and first-block? pre-block?)
-                  (let [content (-> (string/trim content)
-                                    ;; FIXME: should only works with :filters
-                                    (string/replace "\"" "\\\""))]
+                  (let [content (string/trim content)]
                     (str content "\n"))
 
                   :else

+ 11 - 6
src/main/frontend/routes.cljs

@@ -9,7 +9,8 @@
             [frontend.components.search :as search]
             [frontend.components.settings :as settings]
             [frontend.components.external :as external]
-            [frontend.components.shortcut :as shortcut]))
+            [frontend.components.shortcut :as shortcut]
+            [frontend.extensions.zotero :as zotero]))
 
 ;; http://localhost:3000/#?anchor=fn.1
 (def routes
@@ -57,6 +58,14 @@
     {:name :settings
      :view settings/settings}]
 
+   ["/settings/shortcut"
+    {:name :shortcut-setting
+     :view shortcut/shortcut}]
+
+   ["/settings/zotero"
+    {:name :zotero-setting
+     :view zotero/settings}]
+
    ["/import"
     {:name :import
      :view external/import-cp}]
@@ -67,8 +76,4 @@
 
    ["/plugins"
     {:name :plugins
-     :view plugins/installed-page}]
-
-   ["/helper/shortcut"
-    {:name :shortcut
-     :view shortcut/shortcut}]])
+     :view plugins/installed-page}]])

+ 13 - 2
src/main/frontend/state.cljs

@@ -83,6 +83,7 @@
       :editor/show-date-picker? false
       ;; With label or other data
       :editor/show-input nil
+      :editor/show-zotero false
       :editor/last-saved-cursor nil
       :editor/editing? nil
       :editor/last-edit-block-input-id nil
@@ -544,6 +545,16 @@
   []
   (get @state :editor/show-input))
 
+
+(defn set-editor-show-zotero!
+  [value]
+  (set-state! :editor/show-zotero value))
+
+(defn get-editor-show-zotero
+  []
+  (get @state :editor/show-zotero))
+
+
 (defn set-edit-input-id!
   [input-id]
   (swap! state update :editor/editing?
@@ -1260,8 +1271,8 @@
   [q]
   (when-not (string/blank? q)
     (update-state! :search/graph-filters
-                  (fn [value]
-                    (vec (distinct (conj value q)))))))
+                   (fn [value]
+                     (vec (distinct (conj value q)))))))
 
 (defn remove-search-filter!
   [q]

+ 10 - 11
src/main/frontend/text.cljs

@@ -86,11 +86,6 @@
   [s]
   (string/split s #"(\"[^\"]*\")"))
 
-(defn- surrounded-by-quotes
-  [s]
-  (and (string? s)
-       (= (first s) (last s) \")))
-
 (def markdown-link #"\[([^\[]+)\](\(.*\))")
 (defn split-page-refs-without-brackets
   ([s]
@@ -108,15 +103,19 @@
      (let [result (->> (sep-by-quotes s)
                        (mapcat
                         (fn [s]
-                          (if (surrounded-by-quotes s)
-                            [s]
+                          (when-not (util/wrapped-by-quotes? (string/trim s))
                             (string/split s page-ref-re-2))))
-                       (mapcat (fn [s] (if (and (string/includes? (string/trimr s) "]],")
-                                               (not (surrounded-by-quotes s)))
+                       (mapcat (fn [s] (cond
+                                        (util/wrapped-by-quotes? s)
+                                        nil
+
+                                        (string/includes? (string/trimr s) "]],")
                                         (let [idx (string/index-of s "]],")]
                                           [(subs s 0 idx)
                                            "]]"
                                            (subs s (+ idx 3))])
+
+                                        :else
                                         [s])))
                        (remove #(= % ""))
                        (mapcat (fn [s] (if (string/ends-with? s "]]")
@@ -127,8 +126,8 @@
                        (remove string/blank?)
                        (mapcat (fn [s]
                                  (cond
-                                   (surrounded-by-quotes s)
-                                   [(subs s 1 (dec (count s)))]
+                                   (util/wrapped-by-quotes? s)
+                                   nil
 
                                    (page-ref? s)
                                    [(if un-brackets? (page-ref-un-brackets! s) s)]

+ 27 - 6
src/main/frontend/util.cljc

@@ -4,6 +4,8 @@
   #?(:cljs (:require
             ["/frontend/selection" :as selection]
             ["/frontend/utils" :as utils]
+            [camel-snake-kebab.core :as csk]
+            [camel-snake-kebab.extras :as cske]
             [cljs-bean.core :as bean]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
@@ -137,12 +139,17 @@
 ;;   [fmt & args]
 ;;   (apply gstring/format fmt args))
 
-(defn json->clj
-  [json-string]
-  #?(:cljs
-     (-> json-string
-         (js/JSON.parse)
-         (js->clj :keywordize-keys true))))
+#?(:cljs
+   (defn json->clj
+     ([json-string]
+      (json->clj json-string false))
+     ([json-string kebab?]
+      (let [m (-> json-string
+                  (js/JSON.parse)
+                  (js->clj :keywordize-keys true))]
+        (if kebab?
+          (cske/transform-keys csk/->kebab-case-keyword m)
+          m)))))
 
 (defn remove-nils
   "remove pairs of key-value that has nil value from a (possibly nested) map."
@@ -1349,3 +1356,17 @@
        (not
         (some #(string/ends-with? path %)
               [".md" ".markdown" ".org" ".edn" ".css"]))))))
+
+(defn wrapped-by-quotes?
+  [v]
+  (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
+
+(defn unquote-string
+  [v]
+  (string/trim (subs v 1 (dec (count v)))))
+
+(defn unquote-string-if-wrapped
+  [v]
+  (if (wrapped-by-quotes? v)
+    (unquote-string v)
+    v))

+ 67 - 63
src/main/frontend/util/property.cljs

@@ -157,6 +157,9 @@
               (not (string/blank? (str value))))
      (let [ast (mldoc/->edn content (mldoc/default-config format))
            title? (mldoc/block-with-title? (ffirst (map first ast)))
+           has-properties? (or (and title?
+                                    (mldoc/properties? (second ast)))
+                               (mldoc/properties? (first ast)))
            lines (string/split-lines content)
            [title body] (if title?
                           [(first lines) (string/join "\n" (rest lines))]
@@ -165,71 +168,69 @@
            key (string/lower-case (name key))
            value (string/trim (str value))
            start-idx (.indexOf lines properties-start)
-           end-idx (.indexOf lines properties-end)]
-       (cond
-         (and org? (not (contains-properties? content)))
-         (let [properties (build-properties-str format {key value})]
-           (if title
-             (str title "\n" properties body)
-             (str properties content)))
-
-         (and (>= start-idx 0) (> end-idx 0) (> end-idx start-idx))
-         (let [exists? (atom false)
-               before (subvec lines 0 start-idx)
-               middle (doall
-                       (->> (subvec lines (inc start-idx) end-idx)
-                            (mapv (fn [text]
-                                    (let [[k v] (util/split-first ":" (subs text 1))]
-                                      (if (and k v)
-                                        (let [key-exists? (= k key)
-                                              _ (when key-exists? (reset! exists? true))
-                                              v (if key-exists? value v)]
-                                          (str ":" k ": "  (string/trim v)))
-                                        text))))))
-               middle (if @exists? middle (conj middle (str ":" key ": "  value)))
-               after (subvec lines (inc end-idx))
-               lines (concat before [properties-start] middle [properties-end] after)]
-           (string/join "\n" lines))
-
-         (not org?)
-         (let [exists? (atom false)
-               sym (if front-matter? ": " ":: ")
-               new-property-s (str key sym  value)
-               property-f (if front-matter? front-matter-property? simplified-property?)
-               groups (partition-by property-f lines)
-               no-properties? (and (= 1 (count groups))
-                                   (not (property-f (ffirst groups))))
-               lines (mapcat (fn [lines]
-                               (if (property-f (first lines))
-                                 (let [lines (doall
+           end-idx (.indexOf lines properties-end)
+           result        (cond
+                           (and org? (not has-properties?))
+                           (let [properties (build-properties-str format {key value})]
+                             (if title
+                               (str title "\n" properties body)
+                               (str properties content)))
+
+                           (and has-properties? (>= start-idx 0) (> end-idx 0) (> end-idx start-idx))
+                           (let [exists? (atom false)
+                                 before (subvec lines 0 start-idx)
+                                 middle (doall
+                                         (->> (subvec lines (inc start-idx) end-idx)
                                               (mapv (fn [text]
-                                                      (let [[k v] (util/split-first sym text)]
+                                                      (let [[k v] (util/split-first ":" (subs text 1))]
                                                         (if (and k v)
                                                           (let [key-exists? (= k key)
                                                                 _ (when key-exists? (reset! exists? true))
                                                                 v (if key-exists? value v)]
-                                                            (str k sym  (string/trim v)))
-                                                          text)))
-                                                    lines))
-                                       lines (if @exists? lines (conj lines new-property-s))]
-                                   lines)
-                                 lines))
-                             groups)
-               lines (if no-properties?
-                       (cond
-                         (string/blank? content)
-                         [new-property-s]
-
-                         title?
-                         (cons (first lines) (cons new-property-s (rest lines)))
-
-                         :else
-                         (cons new-property-s lines))
-                       lines)]
-           (string/join "\n" lines))
-
-         :else
-         content)))))
+                                                            (str ":" k ": "  (string/trim v)))
+                                                          text))))))
+                                 middle (if @exists? middle (conj middle (str ":" key ": "  value)))
+                                 after (subvec lines (inc end-idx))
+                                 lines (concat before [properties-start] middle [properties-end] after)]
+                             (string/join "\n" lines))
+
+                           (not org?)
+                           (let [exists? (atom false)
+                                 sym (if front-matter? ": " ":: ")
+                                 new-property-s (str key sym  value)
+                                 property-f (if front-matter? front-matter-property? simplified-property?)
+                                 groups (partition-by property-f lines)
+                                 compose-lines (fn []
+                                                 (mapcat (fn [lines]
+                                                           (if (property-f (first lines))
+                                                             (let [lines (doall
+                                                                          (mapv (fn [text]
+                                                                                  (let [[k v] (util/split-first sym text)]
+                                                                                    (if (and k v)
+                                                                                      (let [key-exists? (= k key)
+                                                                                            _ (when key-exists? (reset! exists? true))
+                                                                                            v (if key-exists? value v)]
+                                                                                        (str k sym  (string/trim v)))
+                                                                                      text)))
+                                                                                lines))
+                                                                   lines (if @exists? lines (conj lines new-property-s))]
+                                                               lines)
+                                                             lines))
+                                                         groups))
+                                 lines (cond
+                                         has-properties?
+                                         (compose-lines)
+
+                                         title?
+                                         (cons (first lines) (cons new-property-s (rest lines)))
+
+                                         :else
+                                         (cons new-property-s lines))]
+                             (string/join "\n" lines))
+
+                           :else
+                           content)]
+       (string/trimr result)))))
 
 (defn insert-properties
   [format content kvs]
@@ -360,7 +361,7 @@
         v (if (or (symbol? v) (keyword? v)) (name v) (str v))
         v (string/trim v)]
     (cond
-      (= k "filters")
+      (contains? #{"title" "filters"} k)
       v
 
       (= v "true")
@@ -371,11 +372,14 @@
       (util/safe-re-find #"^\d+$" v)
       (util/safe-parse-int v)
 
-      (and (= "\"" (first v) (last v))) ; wrapped in ""
-      (string/trim (subs v 1 (dec (count v))))
+      (util/wrapped-by-quotes? v) ; wrapped in ""
+      (util/unquote-string v)
 
       (contains? @non-parsing-properties (string/lower-case k))
       v
 
+      (string/starts-with? v "http")
+      v
+
       :else
       (text/split-page-refs-without-brackets v))))

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.2.10")
+(defonce version "0.3.0")

+ 285 - 0
src/test/fixtures/zotero.edn

@@ -0,0 +1,285 @@
+{:journal-article-sample-1
+ {:key     "JAHCZRNB",
+  :version 37,
+  :library
+  {:type "user",
+   :id   8234867,
+   :name "weihua-lu",
+   :links
+   {:alternate {:href "https://www.zotero.org/weihua-lu", :type "text/html"}}},
+  :links
+  {:self
+   {:href "https://api.zotero.org/users/8234867/items/JAHCZRNB",
+    :type "application/json"},
+   :alternate
+   {:href "https://www.zotero.org/weihua-lu/items/JAHCZRNB", :type "text/html"},
+   :attachment
+   {:href            "https://api.zotero.org/users/8234867/items/PX7ERZ5D",
+    :type            "application/json",
+    :attachment-type "application/pdf",
+    :attachment-size 676443}},
+  :meta
+  {:creator-summary "Efroni et al.",
+   :parsed-date     "2019-02-17",
+   :num-children    2},
+  :data
+  {:tags
+   [{:tag "Computer Science - Artificial Intelligence", :type 1}
+    {:tag "Computer Science - Machine Learning", :type 1}
+    {:tag "Statistics - Machine Learning", :type 1}],
+   :creators
+   [{:creator-type "author", :first-name "Yonathan", :last-name "Efroni"}
+    {:creator-type "author", :first-name "Gal", :last-name "Dalal"}
+    {:creator-type "author", :first-name "Bruno", :last-name "Scherrer"}
+    {:creator-type "author", :first-name "Shie", :last-name "Mannor"}],
+   :date                 "2019-02-17",
+   :issn                 "",
+   :archive-location     "",
+   :series-text          "",
+   :issue                "",
+   :key                  "JAHCZRNB",
+   :series-title         "",
+   :relations            {},
+   :series               "",
+   :date-modified        "2021-07-12T08:04:52Z",
+   :extra                "arXiv: 1809.01843\nCitation Key: efroniHowCombineTreeSearch2019",
+   :doi                  "",
+   :collections          ["ILULWK4S"],
+   :title                "How to Combine Tree-Search Methods in Reinforcement Learning",
+   :pages                "",
+   :volume               "",
+   :item-type            "journalArticle",
+   :access-date          "2021-07-12T08:04:51Z",
+   :call-number          "",
+   :rights               "",
+   :language             "en",
+   :url                  "http://arxiv.org/abs/1809.01843",
+   :short-title          "",
+   :abstract-note
+   "Finite-horizon lookahead policies are abundantly used in Reinforcement Learning and demonstrate impressive empirical success. Usually, the lookahead policies are implemented with specific planning methods such as Monte Carlo Tree Search (e.g. in AlphaZero (Silver et al. 2017b)). Referring to the planning problem as tree search, a reasonable practice in these implementations is to back up the value only at the leaves while the information obtained at the root is not leveraged other than for updating the policy. Here, we question the potency of this approach. Namely, the latter procedure is non-contractive in general, and its convergence is not guaranteed. Our proposed enhancement is straightforward and simple: use the return from the optimal tree path to back up the values at the descendants of the root. This leads to a γh-contracting procedure, where γ is the discount factor and h is the tree depth. To establish our results, we first introduce a notion called multiple-step greedy consistency. We then provide convergence rates for two algorithmic instantiations of the above enhancement in the presence of noise injected to both the tree search stage and value estimation stage.",
+   :publication-title    "arXiv:1809.01843 [cs, stat]",
+   :date-added           "2021-07-12T08:04:51Z",
+   :version              37,
+   :archive              "",
+   :journal-abbreviation "",
+   :library-catalog      "arXiv.org"}}
+
+ :journal-article-sample-2
+ {:key     "54QV68M6",
+  :version 1,
+  :library
+  {:type "user",
+   :id   475425,
+   :name "Z public library",
+   :links
+   {:alternate
+    {:href "https://www.zotero.org/z_public_library", :type "text/html"}}},
+  :links
+  {:self
+   {:href "https://api.zotero.org/users/475425/items/54QV68M6",
+    :type "application/json"},
+   :alternate
+   {:href "https://www.zotero.org/z_public_library/items/54QV68M6",
+    :type "text/html"}},
+  :meta
+  {:creator-summary "Lan Levengood et al.",
+   :parsed-date     "2010-08",
+   :num-children    0},
+  :data
+  {:tags
+   [{:tag "Animals", :type 1}
+    {:tag "Bone Morphogenetic Protein 2", :type 1}
+    {:tag "Bone and Bones", :type 1}
+    {:tag "Calcium Phosphates", :type 1}
+    {:tag "Cell Count", :type 1}
+    {:tag "Gelatin", :type 1}
+    {:tag "Humans", :type 1}
+    {:tag "Microscopy, Electron, Scanning", :type 1}
+    {:tag "Microspheres", :type 1}
+    {:tag "Organ Size", :type 1}
+    {:tag "Osseointegration", :type 1}
+    {:tag "Osteogenesis", :type 1}
+    {:tag "Porosity", :type 1}
+    {:tag "Robotics", :type 1}
+    {:tag "Sus scrofa", :type 1}
+    {:tag "Tissue Scaffolds", :type 1}
+    {:tag "Tomography, X-Ray Computed", :type 1}],
+   :creators
+   [{:creator-type "author", :first-name "Sheeny K", :last-name "Lan Levengood"}
+    {:creator-type "author", :first-name "Samantha J", :last-name "Polak"}
+    {:creator-type "author", :first-name "Michael J", :last-name "Poellmann"}
+    {:creator-type "author", :first-name "David J", :last-name "Hoelzle"}
+    {:creator-type "author", :first-name "Aaron J", :last-name "Maki"}
+    {:creator-type "author", :first-name "Sherrie G", :last-name "Clark"}
+    {:creator-type "author", :first-name "Matthew B", :last-name "Wheeler"}
+    {:creator-type "author",
+     :first-name   "Amy J",
+     :last-name    "Wagoner Johnson"}],
+   :date                 "Aug 2010",
+   :issn                 "1878-7568",
+   :archive-location     "",
+   :series-text          "",
+   :issue                "8",
+   :key                  "54QV68M6",
+   :series-title         "",
+   :relations            {:owl:same-as "http://zotero.org/groups/36222/items/H35VBPPB"},
+   :series               "",
+   :date-modified        "2011-04-11T19:28:58Z",
+   :extra                "Hey, this is the best article here!",
+   :doi                  "10.1016/j.actbio.2010.02.026",
+   :collections          ["2GUIGKC9"],
+   :title
+   "The effect of BMP-2 on micro- and macroscale osteointegration of biphasic calcium phosphate scaffolds with multiscale porosity",
+   :pages                "3283-3291",
+   :volume               "6",
+   :item-type            "journalArticle",
+   :access-date          "2011-01-13T02:46:37Z",
+   :call-number          "",
+   :rights               "",
+   :language             "",
+   :url                  "http://www.ncbi.nlm.nih.gov/pubmed/20176148",
+   :short-title          "",
+   :abstract-note
+   "It is well established that scaffolds for applications in bone tissue engineering require interconnected pores on the order of 100 microm for bone in growth and nutrient and waste transport. As a result, most studies have focused on scaffold macroporosity (>100 microm). More recently researchers have investigated the role of microporosity in calcium phosphate -based scaffolds. Osteointegration into macropores improves when scaffold rods or struts contain micropores, typically defined as pores less than approximately 50 microm. We recently demonstrated multiscale osteointegration, or growth into both macropores and intra-red micropores (<10 microm), of biphasic calcium phosphate (BCP) scaffolds. The combined effect of BMP-2, a potent osteoinductive growth factor, and multiscale porosity has yet to be investigated. In this study we implanted BCP scaffolds into porcine mandibular defects for 3, 6, 12 and 24 weeks and evaluated the effect of BMP-2 on multiscale osteointegration. The results showed that given this in vivo model BMP-2 influences osteointegration at the microscale, but not at the macroscale, but not at the macroscale. Cell density was higher in the rod micropores for scaffolds containing BMP-2 compared with controls at all time points, but BMP-2 was not required for bone formation in micropores. In contrast, there was essentially no difference in the fraction of bone in macropores for scaffolds with BMP-2 compared with controls. Additionally, bone in macropores seemed to have reached steady-state by 3 weeks. Multiscale osteointegration results in bone-scaffold composites that are fully osteointegrated, with no 'dead space'. These composites are likely to contain a continuous cell network as well as the potential for enhanced load transfer and improved mechanical properties.",
+   :publication-title    "Acta Biomaterialia",
+   :date-added           "2011-01-13T02:46:37Z",
+   :version              1,
+   :archive              "",
+   :journal-abbreviation "Acta Biomater",
+   :library-catalog      "NCBI PubMed"}}
+
+ :newspaper-article-sample-1
+ {:key     "U4TU25IC",
+  :version 1,
+  :library
+  {:type "user",
+   :id   475425,
+   :name "Z public library",
+   :links
+   {:alternate
+    {:href "https://www.zotero.org/z_public_library", :type "text/html"}}},
+  :links
+  {:self
+   {:href "https://api.zotero.org/users/475425/items/U4TU25IC",
+    :type "application/json"},
+   :alternate
+   {:href "https://www.zotero.org/z_public_library/items/U4TU25IC",
+    :type "text/html"}},
+  :meta    {:parsed-date "2011-03-28", :num-children 1},
+  :data
+  {:tags
+   [{:tag "Computers and the Internet", :type 1}
+    {:tag "New York Times", :type 1}
+    {:tag "Newspapers", :type 1}
+    {:tag "Prices (Fares, Fees and Rates)", :type 1}],
+   :creators          [],
+   :date              "March 28, 2011",
+   :issn              "0362-4331",
+   :archive-location  "",
+   :key               "U4TU25IC",
+   :place             "",
+   :relations         {},
+   :date-modified     "2011-03-29T17:48:57Z",
+   :section           "Opinion",
+   :extra             "",
+   :collections       ["3DKBJE5C"],
+   :title             "A Letter to Our Readers About Digital Subscriptions",
+   :pages             "",
+   :item-type         "newspaperArticle",
+   :access-date       "2011-03-29T17:48:57Z",
+   :call-number       "",
+   :rights            "",
+   :language          "",
+   :url               "http://www.nytimes.com/2011/03/28/opinion/l28times.html?_r=1",
+   :short-title       "",
+   :abstract-note     "",
+   :edition           "",
+   :publication-title "The New York Times",
+   :date-added        "2011-03-29T17:48:57Z",
+   :version           1,
+   :archive           "",
+   :library-catalog   "NYTimes.com"}}
+
+ :book-sample-1
+ {:key     "J6NP6VJW",
+  :version 1,
+  :library
+  {:type "user",
+   :id   475425,
+   :name "Z public library",
+   :links
+   {:alternate
+    {:href "https://www.zotero.org/z_public_library", :type "text/html"}}},
+  :links
+  {:self
+   {:href "https://api.zotero.org/users/475425/items/J6NP6VJW",
+    :type "application/json"},
+   :alternate
+   {:href "https://www.zotero.org/z_public_library/items/J6NP6VJW",
+    :type "text/html"}},
+  :meta    {:creator-summary "Orwell", :parsed-date "1984", :num-children 0},
+  :data
+  {:tags              [],
+   :creators
+   [{:creator-type "author", :first-name "George", :last-name "Orwell"}],
+   :date              "1984",
+   :archive-location  "",
+   :publisher         "Index",
+   :key               "J6NP6VJW",
+   :place             "",
+   :relations         {},
+   :series            "",
+   :date-modified     "2011-05-16T14:36:12Z",
+   :extra             "",
+   :isbn              "",
+   :collections       ["BX9965IJ"],
+   :title             "1984",
+   :volume            "",
+   :series-number     "",
+   :item-type         "book",
+   :access-date       "",
+   :call-number       "",
+   :rights            "",
+   :language          "",
+   :url               "",
+   :number-of-volumes "",
+   :short-title       "",
+   :abstract-note     "",
+   :edition           "",
+   :date-added        "2011-05-16T14:36:12Z",
+   :version           1,
+   :archive           "",
+   :library-catalog   "",
+   :num-pages         ""}}
+
+ :note-sample-1
+ {:key     "A6ADHHQQ",
+  :version 4,
+  :library
+  {:type "user",
+   :id   8234867,
+   :name "weihua-lu",
+   :links
+   {:alternate {:href "https://www.zotero.org/weihua-lu", :type "text/html"}}},
+  :links
+  {:self
+   {:href "https://api.zotero.org/users/8234867/items/A6ADHHQQ",
+    :type "application/json"},
+   :alternate
+   {:href "https://www.zotero.org/weihua-lu/items/A6ADHHQQ",
+    :type "text/html"},
+   :up
+   {:href "https://api.zotero.org/users/8234867/items/AU8RUSW7",
+    :type "application/json"}},
+  :meta    {:num-children 0},
+  :data
+  {:tags          [],
+   :key           "A6ADHHQQ",
+   :relations     {},
+   :date-modified "2021-07-01T06:04:55Z",
+   :note
+   "<div data-schema-version=\"3\"><p>This study shows how Tekster [Texter], a strategy-focused writing instruction program, improves the writing performance of students in Grade 4 to 6. This positive effect was still visible 2 months after the intervention. As the intervention was successfully implemented by teachers in a large number of classrooms, this study suggests that Tekster is a promising approach for improving students’ writing in general education.</p>\n</div>",
+   :item-type     "note",
+   :date-added    "2021-07-01T06:04:42Z",
+   :version       4,
+   :parent-item   "AU8RUSW7"}}}

+ 2 - 2
src/test/frontend/db/query_dsl_test.cljs

@@ -321,7 +321,7 @@ last-modified-at:: 1609084800002"}]]
       "(not [[page 1]])"
       {:query '([?b :block/uuid]
                 (not [?b :block/path-refs [:block/name "page 1"]]))
-       :count 31}))
+       :count 33}))
 
   (testing "Between query"
     (are [x y] (= (count-only x) y)
@@ -369,7 +369,7 @@ last-modified-at:: 1609084800002"}]]
                   (and [?b :block/path-refs [:block/name "page 1"]])
                   (and [?b :block/path-refs [:block/name "page 2"]])
                   [?b])))
-       :count 34})
+       :count 36})
 
     ;; FIXME: not working
     ;; (are [x y] (= (q-count x) y)

+ 73 - 0
src/test/frontend/extensions/zotero/extractor_test.cljs

@@ -0,0 +1,73 @@
+;; FIXME
+;; https://github.com/davidsantiago/hickory/issues/17
+;; hictory doesnt work in Node
+;; (ns frontend.extensions.zotero.extractor-test
+;;   (:require [clojure.edn :as edn]
+;;             [clojure.test :as test :refer [deftest is testing]]
+;;             [shadow.resource :as rc]
+;;             [clojure.string :as str]
+;;             [frontend.extensions.zotero.extractor :as extractor]))
+
+;; (def data
+;;   (-> (rc/inline "fixtures/zotero.edn")
+;;       (edn/read-string)))
+
+;; (deftest extract-test
+;;   (testing "journal article"
+;;     (let [{:keys [page-name properties]}
+;;           (extractor/extract (:journal-article-sample-1 data))]
+
+;;       (testing "page name prefer citation key"
+;;         (is (= "@efroniHowCombineTreeSearch2019" page-name)))
+
+;;       (testing "convert date"
+;;         (is (= "[[Feb 17th, 2019]]" (-> properties :date))))
+
+;;       (testing "convert date"
+;;         (is (= "[[Feb 17th, 2019]]" (-> properties :date))))
+
+;;       (testing "original title"
+;;         (is (= "How to Combine Tree-Search Methods in Reinforcement Learning" (-> properties :original-title))))
+
+;;       (testing "double quote when containing comma"
+;;         (is (= "\"arXiv:1809.01843 [cs, stat]\"" (-> properties :publication-title))))
+
+;;       (testing "skip when containing newline"
+;;         (is (nil? (-> properties :extra))))))
+
+;;   (testing "another journal article"
+;;     (let [{:keys [page-name properties]}
+;;           (extractor/extract (:journal-article-sample-2 data))
+;;           authors (count (re-seq #"\[\[" (-> properties :authors)))
+;;           tags    (count (re-seq #"\[\[" (-> properties :tags)))]
+
+;;       (testing "authors"
+;;         (is (= 8 authors)))
+
+;;       (testing "tags"
+;;         (is (= 17 tags)))))
+
+;;   (testing "book"
+;;     (let [{:keys [page-name properties]}
+;;           (extractor/extract (:book-sample-1 data))]
+
+;;       (testing "page name"
+;;         (is (= "@1984" page-name)))
+
+;;       (testing "author"
+;;         (is (= "[[George Orwell]]" (-> properties :authors))))
+
+;;       (testing "preserve unparsable date"
+;;         (is (= "1984" (-> properties :date))))))
+
+;;   (testing "newpaper article"
+;;     (let [{:keys [page-name properties]}
+;;           (extractor/extract (:newspaper-article-sample-1 data))]
+;;       (is (= "A Letter to Our Readers About Digital Subscriptions" (-> properties :original-title)))
+
+;;       (testing "use parsed date when possible"
+;;         (is (= "[[Mar 28th, 2011]]" (-> properties :date))))))
+
+;;   (testing "note"
+;;     (let [result (extractor/extract (:note-sample-1 data))]
+;;       (is (str/starts-with? result "This study shows")))))

+ 1 - 1
src/test/frontend/format/block_test.cljs

@@ -25,7 +25,7 @@
 
   (are [x y] (= (vec (:page-refs (block/extract-properties x))) y)
     [["year" "1000"]] []
-    [["year" "\"1000\""]] ["1000"]
+    [["year" "\"1000\""]] []
     [["foo" "[[bar]] test"]] ["bar" "test"]
     [["foo" "[[bar]] test [[baz]]"]] ["bar" "test" "baz"]
     [["foo" "[[bar]] test [[baz]] [[nested [[baz]]]]"]] ["bar" "test" "baz" "nested [[baz]]"]

+ 3 - 3
src/test/frontend/util/property_test.cljs

@@ -50,10 +50,10 @@
 (deftest test-insert-property
   (are [x y] (= x y)
     (property/insert-property :org "hello" "a" "b")
-    "hello\n:PROPERTIES:\n:a: b\n:END:\n"
+    "hello\n:PROPERTIES:\n:a: b\n:END:"
 
     (property/insert-property :org "hello" "a" false)
-    "hello\n:PROPERTIES:\n:a: false\n:END:\n"
+    "hello\n:PROPERTIES:\n:a: false\n:END:"
 
     (property/insert-property :org "hello\n:PROPERTIES:\n:a: b\n:END:\n" "c" "d")
     "hello\n:PROPERTIES:\n:a: b\n:c: d\n:END:"
@@ -111,7 +111,7 @@
     "hello\n:PROPERTIES:\n:foo: bar\n:nice: bingo\n:END:"
     "hello\nfoo:: bar\nnice:: bingo"
 
-    "hello\n:PROPERTIES:\n:foo: bar\n:nice: bingo\n:END:\n"
+    "hello\n:PROPERTIES:\n:foo: bar\n:nice: bingo\n:END:"
     "hello\nfoo:: bar\nnice:: bingo"
 
     "hello\n:PROPERTIES:\n:foo: bar\n:nice: bingo\n:END:\nnice"

+ 331 - 0
templates/zotero-items.edn

@@ -0,0 +1,331 @@
+[{:item-type "artwork",
+  :fields
+  #{"url" "artworkSize" "abstractNote" "libraryCatalog" "archive" "rights"
+    "accessDate" "title" "date" "extra" "shortTitle" "artworkMedium"
+    "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "artist", :primary true} {:creator-type "contributor"}]}
+ {:item-type "attachment",
+  :fields #{"url" "accessDate" "title"},
+  :creator-types []}
+ {:item-type "audioRecording",
+  :fields
+  #{"url" "audioRecordingFormat" "place" "abstractNote" "libraryCatalog"
+    "label" "numberOfVolumes" "archive" "rights" "accessDate" "title"
+    "seriesTitle" "volume" "date" "extra" "shortTitle" "archiveLocation" "ISBN"
+    "callNumber" "language" "runningTime"},
+  :creator-types
+  [{:creator-type "performer", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "composer"}
+   {:creator-type "wordsBy"}]}
+ {:item-type "bill",
+  :fields
+  #{"url" "legislativeBody" "abstractNote" "section" "rights" "billNumber"
+    "accessDate" "codeVolume" "title" "history" "date" "extra" "shortTitle"
+    "session" "language" "code" "codePages"},
+  :creator-types
+  [{:creator-type "sponsor", :primary true}
+   {:creator-type "cosponsor"}
+   {:creator-type "contributor"}]}
+ {:item-type "blogPost",
+  :fields
+  #{"url" "websiteType" "abstractNote" "rights" "blogTitle" "accessDate"
+    "title" "date" "extra" "shortTitle" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "commenter"}
+   {:creator-type "contributor"}]}
+ {:item-type "book",
+  :fields
+  #{"url" "place" "abstractNote" "libraryCatalog" "numberOfVolumes" "archive"
+    "rights" "accessDate" "title" "edition" "publisher" "numPages" "series"
+    "volume" "date" "extra" "shortTitle" "archiveLocation" "ISBN" "callNumber"
+    "language" "seriesNumber"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "bookSection",
+  :fields
+  #{"url" "bookTitle" "pages" "place" "abstractNote" "libraryCatalog"
+    "numberOfVolumes" "archive" "rights" "accessDate" "title" "edition"
+    "publisher" "series" "volume" "date" "extra" "shortTitle" "archiveLocation"
+    "ISBN" "callNumber" "language" "seriesNumber"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "bookAuthor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "case",
+  :fields
+  #{"reporterVolume" "reporter" "url" "court" "abstractNote" "docketNumber"
+    "rights" "accessDate" "caseName" "firstPage" "history" "extra" "shortTitle"
+    "language" "dateDecided"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "counsel"}
+   {:creator-type "contributor"}]}
+ {:item-type "computerProgram",
+  :fields
+  #{"url" "company" "place" "abstractNote" "libraryCatalog"
+    "programmingLanguage" "archive" "rights" "accessDate" "title" "seriesTitle"
+    "date" "extra" "shortTitle" "archiveLocation" "ISBN" "callNumber" "system"
+    "versionNumber"},
+  :creator-types
+  [{:creator-type "programmer", :primary true} {:creator-type "contributor"}]}
+ {:item-type "conferencePaper",
+  :fields
+  #{"url" "pages" "place" "abstractNote" "libraryCatalog" "DOI"
+    "proceedingsTitle" "conferenceName" "archive" "rights" "accessDate" "title"
+    "publisher" "series" "volume" "date" "extra" "shortTitle" "archiveLocation"
+    "ISBN" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "dictionaryEntry",
+  :fields
+  #{"url" "pages" "place" "abstractNote" "libraryCatalog" "numberOfVolumes"
+    "dictionaryTitle" "archive" "rights" "accessDate" "title" "edition"
+    "publisher" "series" "volume" "date" "extra" "shortTitle" "archiveLocation"
+    "ISBN" "callNumber" "language" "seriesNumber"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "document",
+  :fields
+  #{"url" "abstractNote" "libraryCatalog" "archive" "rights" "accessDate"
+    "title" "publisher" "date" "extra" "shortTitle" "archiveLocation"
+    "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "reviewedAuthor"}]}
+ {:item-type "email",
+  :fields
+  #{"url" "abstractNote" "rights" "subject" "accessDate" "date" "extra"
+    "shortTitle" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "recipient"}]}
+ {:item-type "encyclopediaArticle",
+  :fields
+  #{"url" "pages" "place" "abstractNote" "libraryCatalog" "numberOfVolumes"
+    "encyclopediaTitle" "archive" "rights" "accessDate" "title" "edition"
+    "publisher" "series" "volume" "date" "extra" "shortTitle" "archiveLocation"
+    "ISBN" "callNumber" "language" "seriesNumber"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "film",
+  :fields
+  #{"url" "abstractNote" "libraryCatalog" "videoRecordingFormat" "archive"
+    "rights" "genre" "accessDate" "title" "distributor" "date" "extra"
+    "shortTitle" "archiveLocation" "callNumber" "language" "runningTime"},
+  :creator-types
+  [{:creator-type "director", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "scriptwriter"}
+   {:creator-type "producer"}]}
+ {:item-type "forumPost",
+  :fields
+  #{"url" "forumTitle" "abstractNote" "rights" "postType" "accessDate" "title"
+    "date" "extra" "shortTitle" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true} {:creator-type "contributor"}]}
+ {:item-type "hearing",
+  :fields
+  #{"documentNumber" "url" "legislativeBody" "pages" "place" "abstractNote"
+    "numberOfVolumes" "rights" "accessDate" "committee" "title" "history"
+    "publisher" "date" "extra" "shortTitle" "session" "language"},
+  :creator-types [{:creator-type "contributor", :primary true}]}
+ {:item-type "instantMessage",
+  :fields
+  #{"url" "abstractNote" "rights" "accessDate" "title" "date" "extra"
+    "shortTitle" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "recipient"}]}
+ {:item-type "interview",
+  :fields
+  #{"url" "abstractNote" "libraryCatalog" "interviewMedium" "archive" "rights"
+    "accessDate" "title" "date" "extra" "shortTitle" "archiveLocation"
+    "callNumber" "language"},
+  :creator-types
+  [{:creator-type "interviewee", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "interviewer"}
+   {:creator-type "translator"}]}
+ {:item-type "journalArticle",
+  :fields
+  #{"url" "ISSN" "issue" "pages" "abstractNote" "libraryCatalog" "DOI"
+    "seriesText" "archive" "rights" "accessDate" "publicationTitle" "title"
+    "series" "seriesTitle" "volume" "date" "extra" "shortTitle"
+    "journalAbbreviation" "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "editor"}
+   {:creator-type "translator"}
+   {:creator-type "reviewedAuthor"}]}
+ {:item-type "letter",
+  :fields
+  #{"url" "letterType" "abstractNote" "libraryCatalog" "archive" "rights"
+    "accessDate" "title" "date" "extra" "shortTitle" "archiveLocation"
+    "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "recipient"}]}
+ {:item-type "magazineArticle",
+  :fields
+  #{"url" "ISSN" "issue" "pages" "abstractNote" "libraryCatalog" "archive"
+    "rights" "accessDate" "publicationTitle" "title" "volume" "date" "extra"
+    "shortTitle" "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "translator"}
+   {:creator-type "reviewedAuthor"}]}
+ {:item-type "manuscript",
+  :fields
+  #{"url" "place" "abstractNote" "libraryCatalog" "archive" "rights"
+    "accessDate" "title" "manuscriptType" "numPages" "date" "extra"
+    "shortTitle" "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "translator"}]}
+ {:item-type "map",
+  :fields
+  #{"url" "place" "scale" "abstractNote" "libraryCatalog" "mapType" "archive"
+    "rights" "accessDate" "title" "edition" "publisher" "seriesTitle" "date"
+    "extra" "shortTitle" "archiveLocation" "ISBN" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "cartographer", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "newspaperArticle",
+  :fields
+  #{"url" "ISSN" "pages" "place" "abstractNote" "libraryCatalog" "section"
+    "archive" "rights" "accessDate" "publicationTitle" "title" "edition" "date"
+    "extra" "shortTitle" "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "translator"}
+   {:creator-type "reviewedAuthor"}]}
+ {:item-type "note", :fields #{}, :creator-types []}
+ {:item-type "patent",
+  :fields
+  #{"url" "country" "references" "pages" "place" "patentNumber" "abstractNote"
+    "issueDate" "filingDate" "applicationNumber" "rights" "accessDate" "title"
+    "assignee" "priorityNumbers" "extra" "legalStatus" "shortTitle" "language"
+    "issuingAuthority"},
+  :creator-types
+  [{:creator-type "inventor", :primary true}
+   {:creator-type "attorneyAgent"}
+   {:creator-type "contributor"}]}
+ {:item-type "podcast",
+  :fields
+  #{"url" "abstractNote" "audioFileType" "rights" "accessDate" "title"
+    "seriesTitle" "extra" "shortTitle" "language" "runningTime"
+    "episodeNumber"},
+  :creator-types
+  [{:creator-type "podcaster", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "guest"}]}
+ {:item-type "presentation",
+  :fields
+  #{"url" "place" "abstractNote" "rights" "accessDate" "title" "date" "extra"
+    "shortTitle" "language" "presentationType" "meetingName"},
+  :creator-types
+  [{:creator-type "presenter", :primary true} {:creator-type "contributor"}]}
+ {:item-type "radioBroadcast",
+  :fields
+  #{"url" "audioRecordingFormat" "place" "abstractNote" "libraryCatalog"
+    "programTitle" "network" "archive" "rights" "accessDate" "title" "date"
+    "extra" "shortTitle" "archiveLocation" "callNumber" "language"
+    "runningTime" "episodeNumber"},
+  :creator-types
+  [{:creator-type "director", :primary true}
+   {:creator-type "scriptwriter"}
+   {:creator-type "producer"}
+   {:creator-type "castMember"}
+   {:creator-type "contributor"}
+   {:creator-type "guest"}]}
+ {:item-type "report",
+  :fields
+  #{"reportType" "url" "reportNumber" "pages" "place" "abstractNote"
+    "libraryCatalog" "archive" "rights" "accessDate" "title" "seriesTitle"
+    "institution" "date" "extra" "shortTitle" "archiveLocation" "callNumber"
+    "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "translator"}
+   {:creator-type "seriesEditor"}]}
+ {:item-type "statute",
+  :fields
+  #{"nameOfAct" "url" "pages" "abstractNote" "dateEnacted" "codeNumber"
+    "section" "rights" "accessDate" "history" "extra" "shortTitle" "session"
+    "language" "publicLawNumber" "code"},
+  :creator-types
+  [{:creator-type "author", :primary true} {:creator-type "contributor"}]}
+ {:item-type "thesis",
+  :fields
+  #{"thesisType" "university" "url" "place" "abstractNote" "libraryCatalog"
+    "archive" "rights" "accessDate" "title" "numPages" "date" "extra"
+    "shortTitle" "archiveLocation" "callNumber" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true} {:creator-type "contributor"}]}
+ {:item-type "tvBroadcast",
+  :fields
+  #{"url" "place" "abstractNote" "libraryCatalog" "programTitle"
+    "videoRecordingFormat" "network" "archive" "rights" "accessDate" "title"
+    "date" "extra" "shortTitle" "archiveLocation" "callNumber" "language"
+    "runningTime" "episodeNumber"},
+  :creator-types
+  [{:creator-type "director", :primary true}
+   {:creator-type "scriptwriter"}
+   {:creator-type "producer"}
+   {:creator-type "castMember"}
+   {:creator-type "contributor"}
+   {:creator-type "guest"}]}
+ {:item-type "videoRecording",
+  :fields
+  #{"studio" "url" "place" "abstractNote" "libraryCatalog" "numberOfVolumes"
+    "videoRecordingFormat" "archive" "rights" "accessDate" "title"
+    "seriesTitle" "volume" "date" "extra" "shortTitle" "archiveLocation" "ISBN"
+    "callNumber" "language" "runningTime"},
+  :creator-types
+  [{:creator-type "director", :primary true}
+   {:creator-type "scriptwriter"}
+   {:creator-type "producer"}
+   {:creator-type "castMember"}
+   {:creator-type "contributor"}]}
+ {:item-type "webpage",
+  :fields
+  #{"url" "websiteTitle" "websiteType" "abstractNote" "rights" "accessDate"
+    "title" "date" "extra" "shortTitle" "language"},
+  :creator-types
+  [{:creator-type "author", :primary true}
+   {:creator-type "contributor"}
+   {:creator-type "translator"}]}]

+ 7 - 0
yarn.lock

@@ -7598,10 +7598,17 @@ react-grid-layout@^0.16.6:
     react-draggable "3.x"
     react-resizable "1.x"
 
[email protected]:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
+  integrity sha1-oZbjP98eeqof2jrvu2i9rZ6Cp50=
+
 react-icons@^2.2.7:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
   integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==
+  dependencies:
+    react-icon-base "2.1.0"
 
 react-is@^16.3.1, react-is@^16.8.1:
   version "16.13.1"