Browse Source

fix(pdf): conflicts

charlie 4 years ago
parent
commit
876277acee
59 changed files with 1660 additions and 527 deletions
  1. 1 0
      .projectile
  2. 1 1
      package.json
  3. 2 0
      resources/css/common.css
  4. 0 0
      resources/js/mhchem.min.js
  5. 1 1
      resources/package.json
  6. 2 1
      src/electron/electron/fs_watcher.cljs
  7. 21 12
      src/electron/electron/utils.cljs
  8. 7 0
      src/main/frontend/commands.cljs
  9. 181 120
      src/main/frontend/components/block.cljs
  10. 34 0
      src/main/frontend/components/block.css
  11. 16 10
      src/main/frontend/components/content.cljs
  12. 7 3
      src/main/frontend/components/export.cljs
  13. 4 0
      src/main/frontend/components/header.cljs
  14. 1 2
      src/main/frontend/components/header.css
  15. 11 0
      src/main/frontend/components/macro.cljs
  16. 101 97
      src/main/frontend/components/page.cljs
  17. 1 1
      src/main/frontend/components/query_table.cljs
  18. 2 1
      src/main/frontend/components/repo.cljs
  19. 4 0
      src/main/frontend/components/right_sidebar.css
  20. 17 17
      src/main/frontend/components/sidebar.cljs
  21. 4 1
      src/main/frontend/components/svg.cljs
  22. 23 1
      src/main/frontend/db/debug.cljs
  23. 72 45
      src/main/frontend/db/model.cljs
  24. 10 9
      src/main/frontend/db/query_dsl.cljs
  25. 13 4
      src/main/frontend/db/query_react.cljs
  26. 1 0
      src/main/frontend/dicts.cljs
  27. 25 19
      src/main/frontend/diff.cljs
  28. 6 4
      src/main/frontend/extensions/calc.cljc
  29. 5 2
      src/main/frontend/extensions/latex.cljs
  30. 636 0
      src/main/frontend/extensions/srs.cljs
  31. 21 0
      src/main/frontend/extensions/srs/handler.cljs
  32. 1 1
      src/main/frontend/format.cljs
  33. 1 1
      src/main/frontend/format/adoc.cljs
  34. 24 22
      src/main/frontend/format/block.cljs
  35. 2 2
      src/main/frontend/format/mldoc.cljs
  36. 1 1
      src/main/frontend/format/protocol.cljs
  37. 66 53
      src/main/frontend/fs/nfs.cljs
  38. 35 20
      src/main/frontend/fs/node.cljs
  39. 0 0
      src/main/frontend/handler/card.cljs
  40. 25 17
      src/main/frontend/handler/editor.cljs
  41. 4 0
      src/main/frontend/handler/events.cljs
  42. 22 26
      src/main/frontend/handler/export.cljs
  43. 1 2
      src/main/frontend/handler/extract.cljs
  44. 1 2
      src/main/frontend/handler/page.cljs
  45. 6 0
      src/main/frontend/handler/ui.cljs
  46. 5 2
      src/main/frontend/handler/web/nfs.cljs
  47. 7 1
      src/main/frontend/modules/outliner/core.cljs
  48. 30 1
      src/main/frontend/modules/shortcut/config.cljs
  49. 9 2
      src/main/frontend/state.cljs
  50. 9 5
      src/main/frontend/ui.cljs
  51. 7 0
      src/main/frontend/ui.css
  52. 53 6
      src/main/frontend/util.cljc
  53. 74 0
      src/main/frontend/util/persist_var.cljs
  54. 14 6
      src/main/frontend/util/property.cljs
  55. 14 0
      src/main/frontend/utils.js
  56. 1 1
      src/main/frontend/version.cljs
  57. 6 0
      tailwind.config.js
  58. 8 1
      templates/config.edn
  59. 4 4
      yarn.lock

+ 1 - 0
.projectile

@@ -9,6 +9,7 @@
 -/resources/static/js/sentry.min.js
 -/resources/static/js/highlight.min.js
 -/resources/static/js/katex.min.js
+-/resources/static/js/mhchem.min.js
 -/resources/static/js/mldoc.min.js
 -/resources/static/js/reveal.min.js
 -/resources/static/js/sci.min.js

+ 1 - 1
package.json

@@ -82,7 +82,7 @@
         "ignore": "^5.1.8",
         "is-svg": "4.2.2",
         "jszip": "^3.5.0",
-        "mldoc": "0.8.9",
+        "mldoc": "0.9.1",
         "path": "^0.12.7",
         "pixi-graph-fork": "^0.1.3",
         "posthog-js": "^1.10.2",

+ 2 - 0
resources/css/common.css

@@ -72,6 +72,7 @@ html[data-theme='dark'] {
   --ls-scrollbar-background-color: rgba(30, 60, 67, 0.1);
   --ls-scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
+  --ls-cloze-text-color: #8fbc8f;
   --ls-icon-color: var(--ls-link-text-color);
   --ls-search-icon-color: var(--ls-link-text-color);
   --ls-a-chosen-bg: var(--ls-secondary-background-color);
@@ -126,6 +127,7 @@ html[data-theme='light'] {
   --ls-scrollbar-background-color: rgba(0, 0, 0, 0.05);
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
+  --ls-cloze-text-color: #0000cd;
   --ls-icon-color: #908e8b;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f7f7f7;

File diff suppressed because it is too large
+ 0 - 0
resources/js/mhchem.min.js


+ 1 - 1
resources/package.json

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

+ 2 - 1
src/electron/electron/fs_watcher.cljs

@@ -23,7 +23,8 @@
   (when (fs/existsSync dir)
     (let [watcher (.watch watcher dir
                           (clj->js
-                           {:ignored (partial utils/ignored-path? dir)
+                           {:ignored (fn [path]
+                                       (utils/ignored-path? dir path))
                             :ignoreInitial true
                             :ignorePermissionErrors true
                             :interval polling-interval

+ 21 - 12
src/electron/electron/utils.cljs

@@ -1,6 +1,8 @@
 (ns electron.utils
   (:require [clojure.string :as string]
-            ["fs" :as fs]))
+            ["fs" :as fs]
+            ["path" :as path]
+            [clojure.string :as string]))
 
 (defonce mac? (= (.-platform js/process) "darwin"))
 (defonce win32? (= (.-platform js/process) "win32"))
@@ -13,18 +15,24 @@
 (defonce open (js/require "open"))
 (defonce fetch (js/require "node-fetch"))
 
-(defn get-file-ext
-  [file]
-  (last (string/split file #"\.")))
-
-;; TODO: ignore according to mime types
 (defn ignored-path?
   [dir path]
-  (or
-   (some #(string/starts-with? path (str dir "/" %))
-         ["." "assets" "node_modules"])
-   (some #(string/ends-with? path %)
-         [".swap" ".crswap" ".tmp" ".DS_Store"])))
+  (when (string? path)
+    (or
+     (some #(string/starts-with? path (str dir "/" %))
+           ["." ".recycle" "assets" "node_modules"])
+     (some #(string/includes? path (str "/" % "/"))
+           ["." ".recycle" "assets" "node_modules"])
+     (string/ends-with? path ".DS_Store")
+     ;; hidden directory or file
+     (re-find #"/\.[^.]+" path)
+     (re-find #"^\.[^.]+" path)
+     (let [path (string/lower-case path)]
+       (and
+        (not (string/blank? (path/extname path)))
+        (not
+         (some #(string/ends-with? path %)
+               [".md" ".markdown" ".org" ".edn" ".css"])))))))
 
 (defn fix-win-path!
   [path]
@@ -35,4 +43,5 @@
 
 (defn read-file
   [path]
-  (.toString (fs/readFileSync path)))
+  (when (fs/existsSync path)
+    (.toString (fs/readFileSync path))))

+ 7 - 0
src/main/frontend/commands.cljs

@@ -37,6 +37,11 @@
                                        :id :label
                                        :placeholder "Label"}]]])
 
+(def *extend-slash-commands (atom []))
+
+(defn register-slash-command [cmd]
+  (swap! *extend-slash-commands conj cmd))
+
 (defn ->marker
   [marker]
   [[:editor/clear-current-slash]
@@ -266,7 +271,9 @@
      ["Embed Vimeo Video" [[:editor/input "{{vimeo }}" {:last-pattern slash
                                                         :backward-pos 2}]]]]
 
+    @*extend-slash-commands
     ;; Allow user to modify or extend, should specify how to extend.
+
     (state/get-commands)
     (state/get-plugins-commands))
    (remove nil?)

+ 181 - 120
src/main/frontend/components/block.cljs

@@ -12,10 +12,12 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.svg :as svg]
+            [frontend.components.macro :as macro]
             [frontend.config :as config]
             [frontend.context.i18n :as i18n]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.utils :as db-utils]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.query-dsl :as query-dsl]
@@ -76,11 +78,8 @@
   (atom nil))
 (def *move-to (atom nil))
 
-;; TODO: Improve blocks grouped by pages
-(defonce max-blocks-per-page 500)
-(defonce virtual-list-scroll-step 450)
-(defonce virtual-list-previous 50)
-
+;; TODO: dynamic
+(defonce max-blocks-per-page 200)
 (defonce *blocks-container-id (atom 0))
 
 ;; TODO:
@@ -398,32 +397,34 @@
 
 (rum/defc page-preview-trigger
   [{:keys [children sidebar? tippy-position tippy-distance fixed-position? open? manual?] :as config} page-name]
-  (let [redirect-page-name (model/get-redirect-page-name page-name (:block/alias? config))
+  (let [redirect-page-name (or (model/get-redirect-page-name page-name (:block/alias? config))
+                               page-name)
         page-original-name (model/get-page-original-name redirect-page-name)
         debounced-open? (use-delayed-open open? page-name)
         html-template (fn []
-                        [:div.tippy-wrapper.overflow-y-auto.p-4
-                         {:style {:width          600
-                                  :text-align     "left"
-                                  :font-weight    500
-                                  :max-height     600
-                                  :padding-bottom 64}}
-                         (if (and (string? page-original-name) (string/includes? page-original-name "/"))
-                           [:div.my-2
-                            (->>
-                             (for [page (string/split page-original-name #"/")]
-                               (when (and (string? page) page)
-                                 (page-reference false page {} nil)))
-                             (interpose [:span.mx-2.opacity-30 "/"]))]
-                           [:h2.font-bold.text-lg (if (= page-name redirect-page-name)
-                                                    page-original-name
-                                                    [:span
-                                                     [:span.text-sm.mr-2 "Alias:"]
-                                                     page-original-name])])
-                         (let [page (db/entity [:block/name (string/lower-case redirect-page-name)])]
-                           (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)
-                           (when-let [f (state/get-page-blocks-cp)]
-                             (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))])]
+                        (when redirect-page-name
+                          [:div.tippy-wrapper.overflow-y-auto.p-4
+                           {:style {:width          600
+                                    :text-align     "left"
+                                    :font-weight    500
+                                    :max-height     600
+                                    :padding-bottom 64}}
+                           (if (and (string? page-original-name) (string/includes? page-original-name "/"))
+                             [:div.my-2
+                              (->>
+                               (for [page (string/split page-original-name #"/")]
+                                 (when (and (string? page) page)
+                                   (page-reference false page {} nil)))
+                               (interpose [:span.mx-2.opacity-30 "/"]))]
+                             [:h2.font-bold.text-lg (if (= page-name redirect-page-name)
+                                                      page-original-name
+                                                      [:span
+                                                       [:span.text-sm.mr-2 "Alias:"]
+                                                       page-original-name])])
+                           (let [page (db/entity [:block/name (string/lower-case redirect-page-name)])]
+                             (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)
+                             (when-let [f (state/get-page-blocks-cp)]
+                               (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))]))]
     (if (or (not manual?) open?)
       (ui/tippy {:html            html-template
                  :interactive     true
@@ -504,6 +505,7 @@
                               (.stopPropagation e))}
        (excalidraw s)]
       [:span.page-reference
+       {:data-ref s}
        (when (and (or show-brackets? nested-link?)
                   (not html-export?)
                   (not contents-page?))
@@ -869,8 +871,9 @@
                         (and (= "File" (first url))
                              "file"))]
           (cond
-            (and (= "Complex" (first url))
-                 (= protocol "id")
+            (and (= (get-in config [:block :block/format]) :org)
+                 (= "Complex" (first url))
+                 (= (string/lower-case protocol) "id")
                  (string? (:link (second url)))
                  (util/uuid-string? (:link (second url)))) ; org mode id
             (let [id (uuid (:link (second url)))
@@ -878,7 +881,7 @@
               (if (:block/pre-block? block)
                 (let [page (:block/page block)]
                   (page-reference html-export? (:block/name page) config label))
-                (block-reference config (:link (second url)) nil)))
+                (block-reference config (:link (second url)) label)))
 
             (= protocol "file")
             (if (show-link? config metadata href full_text)
@@ -1002,7 +1005,7 @@
         [:div.dsl-query
          (let [query (string/join ", " arguments)]
            (custom-query (assoc config :dsl-query? true)
-                         {:title [:span.font-medium.p-1.query-title
+                         {:title [:span.font-medium.px-2.py-1.query-title.text-sm.rounded-md.shadow-xs
                                   (str "Query: " query)]
                           :query query}))]
 
@@ -1112,6 +1115,9 @@
             :else                       ;TODO: maybe collections?
             nil))
 
+        (get @macro/macros name)
+        ((get @macro/macros name) config options)
+
         :else
         (if-let [block-uuid (:block/uuid config)]
           (let [format (get-in config [:block :block/format] :markdown)
@@ -1242,7 +1248,7 @@
                  (:block/uuid child)))))]))))
 
 (rum/defcs block-control < rum/reactive
-  [state config block uuid block-id body children collapsed? *ref-collapsed? *control-show?]
+  [state config block uuid block-id body children collapsed? *ref-collapsed? *control-show? edit-input-id]
   (let [has-children-blocks? (and (coll? children) (seq children))
         has-child? (and
                     (not (:pre-block? block))
@@ -1255,7 +1261,12 @@
         ref-collapsed? (util/react *ref-collapsed?)
         dark? (= "dark" (state/sub :ui/theme))
         ref? (:ref? config)
-        collapsed? (if ref? ref-collapsed? collapsed?)]
+        collapsed? (if ref? ref-collapsed? collapsed?)
+        empty-content? (string/blank?
+                        (property/remove-built-in-properties
+                         (:block/format block)
+                         (:block/content block)))
+        edit? (state/sub [:editor/editing? edit-input-id])]
     [:div.mr-2.flex.flex-row.items-center
      {:style {:height 24
               :margin-top 0
@@ -1276,20 +1287,25 @@
                          (editor-handler/collapse-block! uuid)))))}
       [:span {:class (if control-show? "control-show" "control-hide")}
        (ui/rotating-arrow collapsed?)]]
-     [:a {:on-click (fn [e]
-                      (bullet-on-click e block config uuid))}
-      [:span.bullet-container.cursor
-       {:id (str "dot-" uuid)
-        :draggable true
-        :on-drag-start (fn [event]
-                         (bullet-drag-start event block uuid block-id))
-        :blockid (str uuid)
-        :class (str (when collapsed? "bullet-closed")
-                    " "
-                    (when (and (:document/mode? config)
-                               (not collapsed?))
-                      "hide-inner-bullet"))}
-       [:span.bullet {:blockid (str uuid)}]]]]))
+     (if (and empty-content? (not edit?)
+              (not (:block/top? block))
+              (not (:block/bottom? block)))
+       [:span.bullet-container]
+
+       [:a {:on-click (fn [e]
+                        (bullet-on-click e block config uuid))}
+        [:span.bullet-container.cursor
+         {:id (str "dot-" uuid)
+          :draggable true
+          :on-drag-start (fn [event]
+                           (bullet-drag-start event block uuid block-id))
+          :blockid (str uuid)
+          :class (str (when collapsed? "bullet-closed")
+                      " "
+                      (when (and (:document/mode? config)
+                                 (not collapsed?))
+                        "hide-inner-bullet"))}
+         [:span.bullet {:blockid (str uuid)}]]])]))
 
 (rum/defc dnd-separator
   [block move-to block-content?]
@@ -1496,8 +1512,8 @@
   [config block]
   (let [properties (walk/keywordize-keys (:block/properties block))
         properties-order (:block/properties-order block)
-        properties (apply dissoc properties property/built-in-properties)
-        properties-order (remove property/built-in-properties properties-order)
+        properties (apply dissoc properties (property/built-in-properties))
+        properties-order (remove (property/built-in-properties) properties-order)
         pre-block? (:block/pre-block? block)
         properties (if pre-block?
                      (let [repo (state/get-current-repo)
@@ -1579,7 +1595,7 @@
                  (and (util/sup? target)
                       (d/has-class? target "fn"))
                  (d/has-class? target "image-resize"))
-        (editor-handler/clear-selection! nil)
+        (editor-handler/clear-selection!)
         (editor-handler/unhighlight-blocks!)
         (let [block (or (db/pull [:block/uuid (:block/uuid block)]) block)
               f #(let [cursor-range (util/caret-range (gdom/getElement block-id))
@@ -1853,11 +1869,7 @@
    :on-drag-leave (fn [event]
                     (block-drag-leave *move-to))
    :on-drop (fn [event]
-              (block-drop event uuid block *move-to))
-   :on-mouse-over (fn [e]
-                    (block-mouse-over e has-child? *control-show? block-id doc-mode?))
-   :on-mouse-leave (fn [e]
-                     (block-mouse-leave e has-child? *control-show? block-id doc-mode?))})
+              (block-drop event uuid block *move-to))})
 
 (defn- build-refs-data-value
   [block refs]
@@ -1882,17 +1894,19 @@
   {:init (fn [state]
            (let [[config block] (:rum/args state)
                  ref-collpased? (boolean
-                                 (and (:ref? config)
-                                      (seq (:block/children block))
-                                      (>= (:ref/level block)
-                                          (state/get-ref-open-blocks-level))))]
+                                 (and
+                                  (seq (:block/children block))
+                                  (or (:custom-query? config)
+                                      (and (:ref? config)
+                                           (>= (:ref/level block)
+                                               (state/get-ref-open-blocks-level))))))]
              (assoc state
                     ::control-show? (atom false)
                     ::ref-collapsed? (atom ref-collpased?))))
    :should-update (fn [old-state new-state]
                     (not= (:block/content (second (:rum/args old-state)))
                           (:block/content (second (:rum/args new-state)))))}
-  [state config {:block/keys [uuid title body meta content page format repo children pre-block? top? properties refs path-refs heading-level level type] :as block}]
+  [state config {:block/keys [uuid title body meta content page format repo children pre-block? top? properties refs path-refs heading-level level type idx] :as block}]
   (let [blocks-container-id (:blocks-container-id config)
         config (update config :block merge block)
 
@@ -1903,7 +1917,8 @@
         heading? (and (= type :heading) heading-level (<= heading-level 6))
         *control-show? (get state ::control-show?)
         *ref-collapsed? (get state ::ref-collapsed?)
-        collapsed? (or @*ref-collapsed? (get properties :collapsed))
+        collapsed? (or @*ref-collapsed?
+                       (get properties :collapsed))
         ref? (boolean (:ref? config))
         breadcrumb-show? (:breadcrumb-show? config)
         sidebar? (boolean (:sidebar? config))
@@ -1919,7 +1934,8 @@
                          (seq body))))
         attrs (on-drag-and-mouse-attrs block uuid top? block-id *move-to has-child? *control-show? doc-mode?)
         data-refs (build-refs-data-value block (remove (set refs) path-refs))
-        data-refs-self (build-refs-data-value block refs)]
+        data-refs-self (build-refs-data-value block refs)
+        edit-input-id (str "edit-block-" blocks-container-id "-" uuid)]
     [:div.ls-block.flex.flex-col.rounded-sm
      (cond->
        {:id block-id
@@ -1950,12 +1966,16 @@
      (when top?
        (dnd-separator-wrapper block block-id slide? true false))
 
-     [:div.flex.flex-row.pr-2 {:class (if heading? "items-baseline" "")}
+     [:div.flex.flex-row.pr-2
+      {:class (if heading? "items-baseline" "")
+       :on-mouse-over (fn [e]
+                        (block-mouse-over e has-child? *control-show? block-id doc-mode?))
+       :on-mouse-leave (fn [e]
+                         (block-mouse-leave e has-child? *control-show? block-id doc-mode?))}
       (when (not slide?)
-        (block-control config block uuid block-id body children collapsed? *ref-collapsed? *control-show?))
+        (block-control config block uuid block-id body children collapsed? *ref-collapsed? *control-show? edit-input-id))
 
-      (let [edit-input-id (str "edit-block-" blocks-container-id "-" uuid)]
-        (block-content-or-editor config block edit-input-id block-id slide? heading-level))]
+      (block-content-or-editor config block edit-input-id block-id slide? heading-level)]
 
      (block-children config children collapsed? *ref-collapsed?)
 
@@ -2152,10 +2172,13 @@
            query-result (and query-atom (rum/react query-atom))
            table? (or (get-in current-block [:block/properties :query-table])
                       (and (string? query) (string/ends-with? (string/trim query) "table")))
+           transformed-query-result (when query-result
+                                      (db/custom-query-result-transform query-result remove-blocks q))
            not-grouped-by-page? (or table?
                                     (and (string? query) (string/includes? query "(by-page false)")))
-           result (when query-result
-                    (db/custom-query-result-transform query-result remove-blocks q not-grouped-by-page?))
+           result (if (and (:block/uuid (first transformed-query-result)) (not not-grouped-by-page?))
+                    (db-utils/group-by-page transformed-query-result)
+                    transformed-query-result)
            _ (when-let [query-result (:query-result config)]
                (let [result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)]
                      (reset! query-result result)))
@@ -2175,6 +2198,9 @@
           (ui/foldable
            [:div.custom-query-title
             title
+            [:span.opacity-60.text-sm.ml-2
+             (str (count transformed-query-result) " results")]]
+           [:div
             (when current-block
               [:div.flex.flex-row.align-items.mt-2 {:on-mouse-down (fn [e] (util/stop e))}
                (when-not page-list?
@@ -2194,47 +2220,46 @@
                                (state/pub-event! [:modal/set-query-properties current-block all-keys])))}
                 [:span.table-query-properties
                  [:span.text-sm.mr-1 "Set properties"]
-                 svg/settings-sm]]])]
-           (cond
-             (and (seq result) view-f)
-             (let [result (try
-                            (sci/call-fn view-f result)
-                            (catch js/Error error
-                              (log/error :custom-view-failed {:error error
-                                                              :result result})
-                              [:div "Custom view failed: "
-                               (str error)]))]
-               (util/hiccup-keywordize result))
-
-             page-list?
-             (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text)
-
-             table?
-             (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text)
-
-             (and (seq result) (or only-blocks? blocks-grouped-by-page?))
-             (->hiccup result (cond-> (assoc config
-                                             :custom-query? true
-                                             ;; :breadcrumb-show? true
-                                             :group-by-page? blocks-grouped-by-page?
-                                             ;; :ref? true
-                                             )
-                                children?
-                                (assoc :ref? true))
-                       {:style {:margin-top "0.25rem"
-                                :margin-left "0.25rem"}})
-
-             (seq result)
-             (let [result (->>
-                           (for [record result]
-                             (if (map? record)
-                               (str (util/pp-str record) "\n")
-                               record))
-                           (remove nil?))]
-               [:pre result])
-
-             :else
-             [:div.text-sm.mt-2.ml-2.font-medium.opacity-50 "Empty"])
+                 svg/settings-sm]]])
+            (cond
+              (and (seq result) view-f)
+              (let [result (try
+                             (sci/call-fn view-f result)
+                             (catch js/Error error
+                               (log/error :custom-view-failed {:error error
+                                                               :result result})
+                               [:div "Custom view failed: "
+                                (str error)]))]
+                (util/hiccup-keywordize result))
+
+              page-list?
+              (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text)
+
+              table?
+              (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text)
+
+              (and (seq result) (or only-blocks? blocks-grouped-by-page?))
+              (->hiccup result (cond-> (assoc config
+                                              :custom-query? true
+                                              :breadcrumb-show? true
+                                              :group-by-page? blocks-grouped-by-page?
+                                              :ref? true)
+                                 children?
+                                 (assoc :ref? true))
+                        {:style {:margin-top "0.25rem"
+                                 :margin-left "0.25rem"}})
+
+              (seq result)
+              (let [result (->>
+                            (for [record result]
+                              (if (map? record)
+                                (str (util/pp-str record) "\n")
+                                record))
+                            (remove nil?))]
+                [:pre result])
+
+              :else
+              [:div.text-sm.mt-2.ml-2.font-medium.opacity-50 "Empty"])]
            collapsed?))]))))
 
 (defn admonition
@@ -2472,6 +2497,51 @@
                      blocks)]
        sections))))
 
+(defn- block-list
+  [config blocks]
+  (for [[idx item] (medley/indexed blocks)]
+    (let [item (->
+                (dissoc item :block/meta)
+                (assoc :block/top? (zero? idx)
+                       :block/bottom? (= (count blocks) (inc idx))))
+          config (assoc config :block/uuid (:block/uuid item))]
+      (rum/with-key
+        (block-container config item)
+        (:block/uuid item)))))
+
+(defonce ignore-scroll? (atom false))
+(rum/defcs lazy-blocks <
+  (rum/local 1 ::page)
+  [state config blocks]
+  (let [*page (get state ::page)
+        segment (->> blocks
+                    (drop (* (dec @*page) max-blocks-per-page))
+                    (take max-blocks-per-page))
+        bottom-reached (fn []
+                         (when (and (= (count segment) max-blocks-per-page)
+                                    (> (count blocks) (* @*page max-blocks-per-page))
+                                    (not @ignore-scroll?))
+                           (swap! *page inc)
+                           (util/scroll-to-top))
+                         (reset! ignore-scroll? false))
+        top-reached (fn []
+                      (when (> @*page 1)
+                        (swap! *page dec)
+                        (reset! ignore-scroll? true)
+                        (js/setTimeout #(util/scroll-to
+                                         (.-scrollHeight (js/document.getElementById "lazy-blocks"))) 100)))]
+    [:div#lazy-blocks
+     (when (> @*page 1)
+       [:div.ml-4.mb-4 [:a#prev.opacity-60.opacity-100.text-sm.font-medium {:on-click top-reached}
+                        "Prev"]])
+     (ui/infinite-list
+      "main-container"
+      (block-list config segment)
+      {:on-load bottom-reached})
+     (when (> (count blocks) (* @*page max-blocks-per-page))
+       [:div.ml-4.mt-4 [:a#more.opacity-60.opacity-100.text-sm.font-medium {:on-click bottom-reached}
+                        "More"]])]))
+
 (rum/defcs blocks-container <
   {:init (fn [state]
            (assoc state ::init-blocks-container-id (atom nil)))}
@@ -2498,16 +2568,7 @@
                                0
                                :else
                                -10)}}
-       (let [first-block (first blocks)
-             first-id (:block/uuid (first blocks))]
-         (for [[idx item] (medley/indexed blocks)]
-           (let [item (->
-                       (dissoc item :block/meta)
-                       (assoc :block/top? (zero? idx)))
-                 config (assoc config :block/uuid (:block/uuid item))]
-             (rum/with-key
-               (block-container config item)
-               (:block/uuid item)))))])))
+       (lazy-blocks config blocks)])))
 
 ;; headers to hiccup
 (defn ->hiccup

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

@@ -440,3 +440,37 @@ a.filter svg {
 .query-title {
     background: var(--ls-page-properties-background-color);
 }
+
+.ls-card {
+    height: 100%;
+}
+
+@media (min-width: 1024px) {
+    .ui__modal-panel .ls-card {
+        min-height: 24rem;
+    }
+}
+
+a[data-ref="card"], .page-reference[data-ref="card"] {
+    opacity: 0.3;
+}
+
+.ls-card a[data-ref="card"], .ls-card .page-reference[data-ref="card"] {
+    display: none;
+}
+
+.cards-title {
+    background: var(--ls-page-properties-background-color);
+    border-radius: 4px;
+    padding: 2px 8px;
+}
+
+span.cloze {
+    color: var(--ls-cloze-text-color);
+}
+
+span.cloze-revealed {
+    color: var(--ls-cloze-text-color);
+    text-decoration: underline;
+    text-underline-position: under;
+}

+ 16 - 10
src/main/frontend/components/content.cljs

@@ -22,7 +22,8 @@
             [frontend.components.export :as export]
             [frontend.context.i18n :as i18n]
             [frontend.text :as text]
-            [frontend.handler.page :as page-handler]))
+            [frontend.handler.page :as page-handler]
+            [frontend.extensions.srs :as srs]))
 
 (defn- set-format-js-loading!
   [format value]
@@ -126,6 +127,7 @@
                     (reset! edit? true))}
        "Make template"))))
 
+
 (rum/defc block-context-menu-content
   [target block-id]
   (rum/with-context [[t] i18n/*tongue-context*]
@@ -179,11 +181,15 @@
                         (state/set-modal! #(export/export-blocks block-id)))}
            "Export")
 
-          (ui/menu-link
-           {:key "Copy as JSON"
-            :on-click (fn [_e]
-                        (export-handler/copy-block-as-json! block-id))}
-           "Copy as JSON")
+          (if (srs/card-block? block)
+            (ui/menu-link
+             {:key "Preview Card"
+              :on-click #(srs/preview [(db/pull [:block/uuid block-id])])}
+             "Preview Card")
+            (ui/menu-link
+             {:key "Make a Card"
+              :on-click #(srs/make-block-a-card! block-id)}
+             "Make a Card"))
 
           (ui/menu-link
            {:key "Cut"
@@ -195,10 +201,10 @@
             (when-let [cmds (state/get-plugins-commands-with-type :block-context-menu-item)]
               (for [[_ {:keys [key label] :as cmd} action pid] cmds]
                 (ui/menu-link
-                  {:key      key
-                   :on-click #(commands/exec-plugin-simple-command!
-                                pid (assoc cmd :uuid block-id) action)}
-                  label))))
+                 {:key      key
+                  :on-click #(commands/exec-plugin-simple-command!
+                              pid (assoc cmd :uuid block-id) action)}
+                 label))))
 
           (when (state/sub [:ui/developer-mode?])
             (ui/menu-link

+ 7 - 3
src/main/frontend/components/export.cljs

@@ -87,6 +87,7 @@
         (case type
           :text (export/export-blocks-as-markdown current-repo root-block-id text-indent-style)
           :opml (export/export-blocks-as-opml current-repo root-block-id)
+          :html (export/export-blocks-as-html current-repo root-block-id)
           (export/export-blocks-as-markdown current-repo root-block-id text-indent-style))]
     [:div.export.w-96.resize
      [:div
@@ -95,8 +96,11 @@
                  :class "mr-2 w-20"
                  :on-click #(reset! *export-block-type :text))
       (ui/button "OPML"
+                 :class "mr-2 w-20"
+                 :on-click #(reset! *export-block-type :opml))
+      (ui/button "HTML"
                  :class "w-20"
-                 :on-click #(reset! *export-block-type :opml))]
+                 :on-click #(reset! *export-block-type :html))]
      [:textarea.overflow-y-auto.h-96 {:value content}]
      (let [options (->> text-indent-style-options
                         (mapv (fn [opt]
@@ -113,8 +117,8 @@
                          (#(reset! *export-block-text-indent-style %) value)))}
          (for [{:keys [label value selected]} options]
            [:option (cond->
-                        {:key   label
-                         :value (or value label)}
+                     {:key   label
+                      :value (or value label)}
                       selected
                       (assoc :selected selected))
             label])]])

+ 4 - 0
src/main/frontend/components/header.cljs

@@ -91,6 +91,10 @@
          {:title (t :shortcut.ui/toggle-right-sidebar)
           :options {:on-click state/toggle-sidebar-open?!}})
 
+       (when current-repo
+         {:title (t :cards-view)
+          :options {:on-click #(state/pub-event! [:modal/show-cards])}})
+
        (when current-repo
          {:title (t :graph-view)
           :options {:href (rfe/href :graph)}

+ 1 - 2
src/main/frontend/components/header.css

@@ -29,7 +29,6 @@
   }
 
   a.button {
-    padding: 0.25rem;
     margin: 0 4px;
   }
 }
@@ -114,7 +113,7 @@
 }
 
 a.button {
-    padding: 0.5rem;
+    padding: 0.25rem;
     opacity: 0.6;
     display: block;
     border-radius: 4px;

+ 11 - 0
src/main/frontend/components/macro.cljs

@@ -0,0 +1,11 @@
+(ns frontend.components.macro)
+
+
+(def macros
+  "Register extended macros here."
+  (atom {}))
+
+(defn register
+  "(FN config options) return Hiccup"
+  [macro-name fn]
+  (swap! macros assoc macro-name fn))

+ 101 - 97
src/main/frontend/components/page.cljs

@@ -249,6 +249,81 @@
              [:a {:href (rfe/href :page {:name name})}
               original-name]])] false)]])))
 
+(defn page-menu
+  [repo t page page-name page-original-name title journal? public? developer-mode?]
+  (let [contents? (= (string/lower-case (str page-name)) "contents")
+        links (fn [] (->>
+                     [(when-not contents?
+                        {:title   (t :page/add-to-favorites)
+                         :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
+
+                      {:title "Go to presentation mode"
+                       :options {:on-click (fn []
+                                             (state/sidebar-add-block!
+                                              repo
+                                              (:db/id page)
+                                              :page-presentation
+                                              {:page page}))}}
+                      (when (and (not contents?)
+                                 (not journal?))
+                        {:title   (t :page/rename)
+                         :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
+
+                      (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
+                        [{:title   (t :page/open-in-finder)
+                          :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
+                         {:title   (t :page/open-with-default-app)
+                          :options {:on-click #(js/window.apis.openPath file-path)}}])
+
+                      (when-not contents?
+                        {:title   (t :page/delete)
+                         :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
+
+                      (when (state/get-current-page)
+                        {:title   (t :export)
+                         :options {:on-click #(state/set-modal! export/export-page)}})
+
+                      (when (util/electron?)
+                        {:title   (t (if public? :page/make-private :page/make-public))
+                         :options {:on-click
+                                   (fn []
+                                     (page-handler/update-public-attribute!
+                                      page-name
+                                      (if public? false true))
+                                     (state/close-modal!))}})
+
+                      (when plugin-handler/lsp-enabled?
+                        (for [[_ {:keys [key label] :as cmd} action pid] (state/get-plugins-commands-with-type :page-menu-item)]
+                          {:title label
+                           :options {:on-click #(commands/exec-plugin-simple-command!
+                                                 pid (assoc cmd :page (state/get-current-page)) action)}}))
+
+                      (when developer-mode?
+                        {:title   "(Dev) Show page data"
+                         :options {:on-click (fn []
+                                               (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
+                                                 (println page-data)
+                                                 (notification/show!
+                                                  [:div
+                                                   [:pre.code page-data]
+                                                   [:br]
+                                                   (ui/button "Copy to clipboard"
+                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
+                                                  :success
+                                                  false)))}})]
+                     (flatten)
+                     (remove nil?)))]
+    (ui/dropdown-with-links
+     (fn [{:keys [toggle-fn]}]
+       [:a.cp__vertical-menu-button
+        {:title    "More options"
+         :on-click toggle-fn}
+        (svg/vertical-dots nil)])
+     links
+     {:modal-class (util/hiccup->class
+                    "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-nowrap.dropdown-overflow-auto.page-drop-options")
+      :z-index     1})))
+
 ;; A page is just a logical block
 (rum/defcs page < rum/reactive
   [state {:keys [repo page-name preview?] :as option}]
@@ -315,90 +390,18 @@
                      page-name
                      path-page-name))]]]
                (when (not config/publishing?)
-                 (let [contents? (= (string/lower-case (str page-name)) "contents")
-                       links (fn [] (->>
-                                     [(when-not contents?
-                                        {:title   (t :page/add-to-favorites)
-                                         :options {:on-click (fn [] (page-handler/handle-add-page-to-contents! page-original-name))}})
-
-                                      {:title "Go to presentation mode"
-                                       :options {:on-click (fn []
-                                                             (state/sidebar-add-block!
-                                                              repo
-                                                              (:db/id page)
-                                                              :page-presentation
-                                                              {:page page}))}}
-                                      (when (and (not contents?)
-                                                 (not journal?))
-                                        {:title   (t :page/rename)
-                                         :options {:on-click #(state/set-modal! (rename-page-dialog title page-name))}})
-
-                                      (when-let [file-path (and (util/electron?) (page-handler/get-page-file-path))]
-                                        [{:title   (t :page/open-in-finder)
-                                          :options {:on-click #(js/window.apis.showItemInFolder file-path)}}
-                                         {:title   (t :page/open-with-default-app)
-                                          :options {:on-click #(js/window.apis.openPath file-path)}}])
-
-                                      (when-not contents?
-                                        {:title   (t :page/delete)
-                                         :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
-
-                                      (when (state/get-current-page)
-                                        {:title   (t :export)
-                                         :options {:on-click #(state/set-modal! export/export-page)}})
-
-                                      (when (util/electron?)
-                                        {:title   (t (if public? :page/make-private :page/make-public))
-                                         :options {:on-click
-                                                   (fn []
-                                                     (page-handler/update-public-attribute!
-                                                      page-name
-                                                      (if public? false true))
-                                                     (state/close-modal!))}})
-
-                                      (when plugin-handler/lsp-enabled?
-                                        (for [[_ {:keys [key label] :as cmd} action pid] (state/get-plugins-commands-with-type :page-menu-item)]
-                                          {:title label
-                                           :options {:on-click #(commands/exec-plugin-simple-command!
-                                                                 pid (assoc cmd :page (state/get-current-page)) action)}}))
-
-                                      (when developer-mode?
-                                        {:title   "(Dev) Show page data"
-                                         :options {:on-click (fn []
-                                                               (let [page-data (with-out-str (pprint/pprint (db/pull (:db/id page))))]
-                                                                 (println page-data)
-                                                                 (notification/show!
-                                                                  [:div
-                                                                   [:pre.code page-data]
-                                                                   [:br]
-                                                                   (ui/button "Copy to clipboard"
-                                                                     :on-click #(.writeText js/navigator.clipboard page-data))]
-                                                                  :success
-                                                                  false)))}})]
-                                     (flatten)
-                                     (remove nil?)))]
-                   [:div.flex.flex-row
-
-                    (when plugin-handler/lsp-enabled?
-                      (plugins/hook-ui-slot :page-head-actions-slotted nil)
-                      (plugins/hook-ui-items :pagebar))
-
-                    [:a.opacity-60.hover:opacity-100.page-op.mr-1
-                     {:title "Search in current page"
-                      :on-click #(route-handler/go-to-search! :page)}
-                     svg/search]
-
-                    (ui/dropdown-with-links
-                     (fn [{:keys [toggle-fn]}]
-                       [:a.cp__vertical-menu-button
-                        {:title    "More options"
-                         :on-click toggle-fn}
-                        (svg/vertical-dots nil)])
-                     links
-                     {:modal-class (util/hiccup->class
-                                    "origin-top-right.absolute.right-0.top-10.mt-2.rounded-md.shadow-lg.whitespace-nowrap.dropdown-overflow-auto.page-drop-options")
-                      :z-index     1})]))])
-
+                 [:div.flex.flex-row
+                  (when plugin-handler/lsp-enabled?
+                    (plugins/hook-ui-slot :page-head-actions-slotted nil)
+                    (plugins/hook-ui-items :pagebar))
+
+                  [:a.opacity-60.hover:opacity-100.page-op.mr-1
+                   {:title "Search in current page"
+                    :on-click #(route-handler/go-to-search! :page)}
+                   svg/search]
+
+                  (page-menu repo t page page-name page-original-name title
+                             journal? public? developer-mode?)])])
             [:div
              (when (and block? (not sidebar?))
                (let [config {:id "block-parent"
@@ -413,23 +416,24 @@
                (page-blocks-cp repo page {:sidebar? sidebar?}))]]
 
            (when-not block?
-             (today-queries repo today? sidebar?))
+             [:div
+              (today-queries repo today? sidebar?)
 
-           (tagged-pages repo page-name)
+              (tagged-pages repo page-name)
 
-           ;; referenced blocks
-           [:div {:key "page-references"}
-            (rum/with-key
-              (reference/references route-page-name false)
-              (str route-page-name "-refs"))]
+              ;; referenced blocks
+              [:div {:key "page-references"}
+               (rum/with-key
+                 (reference/references route-page-name false)
+                 (str route-page-name "-refs"))]
 
-           (when (text/namespace-page? route-page-name)
-             (hierarchy/structures route-page-name))
+              (when (text/namespace-page? route-page-name)
+                (hierarchy/structures route-page-name))
 
-           ;; TODO: or we can lazy load them
-           (when-not sidebar?
-             [:div {:key "page-unlinked-references"}
-              (reference/unlinked-references route-page-name)])])))))
+              ;; TODO: or we can lazy load them
+              (when-not sidebar?
+                [:div {:key "page-unlinked-references"}
+                 (reference/unlinked-references route-page-name)])])])))))
 
 (defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
 

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

@@ -34,7 +34,7 @@
 (defn get-keys
   [result page?]
   (let [keys (->> (distinct (mapcat keys (map :block/properties result)))
-                  (remove property/built-in-properties)
+                  (remove (property/built-in-properties))
                   (remove #{:template}))
         keys (if page? (cons :page keys) (cons :block keys))
         keys (concat keys [:created-at :updated-at])]

+ 2 - 1
src/main/frontend/components/repo.cljs

@@ -207,7 +207,8 @@
                [:span.repo-plus svg/plus]
                (let [repo-name (get-repo-name current-repo)
                      repo-name (if (util/electron?)
-                                 (last (string/split repo-name #"/"))
+                                 (last
+                                  (string/split repo-name #"/"))
                                  repo-name)]
                  [:span#repo-name repo-name])
                [:span.dropdown-caret.ml-1 {:style {:border-top-color "#6b7280"}}]]])

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

@@ -10,3 +10,7 @@ html[data-theme=light] {
     }
   }
 }
+
+.sidebar-item-list {
+     padding-bottom: 150px;
+}

+ 17 - 17
src/main/frontend/components/sidebar.cljs

@@ -284,12 +284,12 @@
   (mixins/modal :modal/show?)
   rum/reactive
   (mixins/event-mixin
-    (fn [state]
-      (mixins/listen state js/window "click"
-                     (fn [e]
-                       ;; hide context menu
-                       (state/hide-custom-context-menu!)
-                       (editor-handler/clear-selection! e)))))
+   (fn [state]
+     (mixins/listen state js/window "click"
+                    (fn [e]
+                      ;; hide context menu
+                      (state/hide-custom-context-menu!)
+                      (editor-handler/clear-selection!)))))
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
         close-fn (fn []
@@ -311,17 +311,17 @@
         home? (= :home route-name)
         default-home (get-default-home-if-valid)]
     (rum/with-context [[t] i18n/*tongue-context*]
-                      (theme/container
-                        {:theme         theme
-                         :route         route-match
-                         :current-repo  current-repo
-                         :nfs-granted?  granted?
-                         :db-restoring? db-restoring?
-                         :sidebar-open? sidebar-open?
-                         :system-theme? system-theme?
-                         :on-click      #(do
-                                           (editor-handler/unhighlight-blocks!)
-                                           (util/fix-open-external-with-shift! %))}
+      (theme/container
+       {:theme         theme
+        :route         route-match
+        :current-repo  current-repo
+        :nfs-granted?  granted?
+        :db-restoring? db-restoring?
+        :sidebar-open? sidebar-open?
+        :system-theme? system-theme?
+        :on-click      (fn [e]
+                         (editor-handler/unhighlight-blocks!)
+                         (util/fix-open-external-with-shift! e))}
 
                         [:div.theme-inner
                          (sidebar-mobile-sidebar

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

@@ -610,4 +610,7 @@
   ([] (view-list 16))
   ([size]
    [:svg.icon {:viewBox "0 0 1024 1024" :width size :height size :fill "none" :stroke "currentColor"}
-    [:path {:d "M134.976 853.312H89.6c-26.56 0-46.912-20.928-46.912-48.256 0-27.392 20.352-48.32 46.912-48.32h45.376c26.624 0 46.912 20.928 46.912 48.32 0 27.328-20.288 48.256-46.912 48.256zM134.976 560.32H89.6C63.04 560.32 42.688 539.392 42.688 512s20.352-48.32 46.912-48.32h45.376c26.624 0 46.912 20.928 46.912 48.32s-20.288 48.32-46.912 48.32zM134.976 267.264H89.6c-26.56 0-46.912-20.928-46.912-48.32 0-27.328 20.352-48.256 46.912-48.256h45.376c26.624 0 46.912 20.928 46.912 48.256 0 27.392-20.288 48.32-46.912 48.32zM311.744 853.312c-26.56 0-46.912-20.928-46.912-48.256 0-27.392 20.352-48.32 46.912-48.32h622.72c26.56 0 46.848 20.928 46.848 48.32 0 27.328-20.288 48.256-46.912 48.256H311.744c1.6 0 1.6 0 0 0zM311.744 560.32c-26.56 0-46.912-20.928-46.912-48.32s20.352-48.32 46.912-48.32h622.72c26.56 0 46.848 20.928 46.848 48.32s-20.288 48.32-46.912 48.32H311.744c1.6 0 1.6 0 0 0zM311.744 267.264c-26.56 0-46.912-20.928-46.912-48.32 0-27.328 20.352-48.256 46.912-48.256h622.72c26.56 0 46.848 20.928 46.848 48.256 0 27.392-20.288 48.32-46.912 48.32H311.744c1.6 0 1.6 0 0 0z" :fill "currentColor"}]]))
+    [:path {:d "M134.976 853.312H89.6c-26.56 0-46.912-20.928-46.912-48.256 0-27.392 20.352-48.32 46.912-48.32h45.376c26.624 0 46.912 20.928 46.912 48.32 0 27.328-20.288 48.256-46.912 48.256zM134.976 560.32H89.6C63.04 560.32 42.688 539.392 42.688 512s20.352-48.32 46.912-48.32h45.376c26.624 0 46.912 20.928 46.912 48.32s-20.288 48.32-46.912 48.32zM134.976 267.264H89.6c-26.56 0-46.912-20.928-46.912-48.32 0-27.328 20.352-48.256 46.912-48.256h45.376c26.624 0 46.912 20.928 46.912 48.256 0 27.392-20.288 48.32-46.912 48.32zM311.744 853.312c-26.56 0-46.912-20.928-46.912-48.256 0-27.392 20.352-48.32 46.912-48.32h622.72c26.56 0 46.848 20.928 46.848 48.32 0 27.328-20.288 48.256-46.912 48.256H311.744c1.6 0 1.6 0 0 0zM311.744 560.32c-26.56 0-46.912-20.928-46.912-48.32s20.352-48.32 46.912-48.32h622.72c26.56 0 46.848 20.928 46.848 48.32s-20.288 48.32-46.912 48.32H311.744c1.6 0 1.6 0 0 0zM311.744 267.264c-26.56 0-46.912-20.928-46.912-48.32 0-27.328 20.352-48.256 46.912-48.256h622.72c26.56 0 46.848 20.928 46.848 48.256 0 27.392-20.288 48.32-46.912 48.32H311.744c1.6 0 1.6 0 0 0z" :fill "currentColor"}]]))
+
+(def arrow-expand
+  (hero-icon "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"))

+ 23 - 1
src/main/frontend/db/debug.cljs

@@ -1,12 +1,34 @@
 (ns frontend.db.debug
   (:require [medley.core :as medley]
-            [frontend.db.utils :as db-utils]))
+            [frontend.db.utils :as db-utils]
+            [frontend.db :as db]
+            [datascript.core :as d]
+            [frontend.util :as util]))
 
 ;; shortcut for query a block with string ref
 (defn qb
   [string-id]
   (db-utils/pull [:block/uuid (medley/uuid string-id)]))
 
+(defn check-left-id-conflicts
+  []
+  (let [db (db/get-conn)
+        blocks (->> (d/datoms db :avet :block/uuid)
+                    (map :v)
+                    (map (fn [id]
+                           (let [e (db-utils/entity [:block/uuid id])]
+                             (if (:block/name e)
+                               nil
+                               {:block/left (:db/id (:block/left e))
+                                :block/parent (:db/id (:block/parent e))}))))
+                    (remove nil?))
+        count-1 (count blocks)
+        count-2 (count (distinct blocks))
+        result (filter #(> (second %) 1) (frequencies blocks))]
+    (assert (= count-1 count-2) (util/format "Blocks count: %d, repeated blocks count: %d"
+                                             count-1
+                                             (- count-1 count-2)))))
+
 (comment
   (defn debug!
     []

+ 72 - 45
src/main/frontend/db/model.cljs

@@ -441,6 +441,71 @@
   (when-let [page (db-utils/entity [:block/name page])]
     (:block/properties page)))
 
+;; FIXME: alert
+(defn- keep-only-one-file
+  [blocks]
+  (filter (fn [b] (= (:block/file b) (:block/file (first blocks)))) blocks))
+
+(defn sort-by-left
+  ([blocks parent]
+   (sort-by-left blocks parent true))
+  ([blocks parent check?]
+   (let [blocks (keep-only-one-file blocks)]
+     (when check?
+       (when (not= (count blocks) (count (set (map :block/left blocks))))
+         (let [duplicates (->> (map (comp :db/id :block/left) blocks)
+                               frequencies
+                               (filter (fn [[_k v]] (> v 1)))
+                               (map (fn [[k _v]]
+                                      (let [left (db-utils/pull k)]
+                                        {:left left
+                                         :duplicates (->>
+                                                      (filter (fn [block]
+                                                                (= k (:db/id (:block/left block))))
+                                                              blocks)
+                                                      (map #(select-keys % [:db/id :block/level :block/content :block/file])))}))))]
+           (util/pprint duplicates)))
+       (assert (= (count blocks) (count (set (map :block/left blocks)))) "Each block should have a different left node"))
+
+    (let [left->blocks (reduce (fn [acc b] (assoc acc (:db/id (:block/left b)) b)) {} blocks)]
+      (loop [block parent
+             result []]
+        (if-let [next (get left->blocks (:db/id block))]
+          (recur next (conj result next))
+          (vec result)))))))
+
+(defn- sort-by-left-recursive
+  [form]
+  (walk/postwalk (fn [f]
+                   (if (and (map? f)
+                            (:block/_parent f))
+                     (let [children (:block/_parent f)]
+                       (-> f
+                           (dissoc :block/_parent)
+                           (assoc :block/children (sort-by-left children f))))
+                     f))
+                 form))
+
+(defn flatten-blocks-sort-by-left
+  [blocks parent]
+  (let [ids->blocks (zipmap (map (fn [b] [(:db/id (:block/parent b))
+                                         (:db/id (:block/left b))]) blocks) blocks)
+        top-block (get ids->blocks [(:db/id parent) (:db/id parent)])]
+    (loop [node parent
+           next-siblings '()
+           result []]
+      (let [id (:db/id node)
+            child-block (get ids->blocks [id id])
+            next-sibling (get ids->blocks [(:db/id (:block/parent node)) id])
+            next-siblings (if (and next-sibling child-block)
+                            (cons next-sibling next-siblings)
+                            next-siblings)]
+        (if-let [node (or child-block next-sibling)]
+          (recur node next-siblings (conj result node))
+          (if-let [sibling (first next-siblings)]
+            (recur sibling (rest next-siblings) (conj result sibling))
+            result))))))
+
 (defn get-page-blocks
   ([page]
    (get-page-blocks (state/get-current-repo) page nil))
@@ -449,9 +514,10 @@
   ([repo-url page {:keys [use-cache? pull-keys]
                    :or {use-cache? true
                         pull-keys '[*]}}]
-   (let [page (string/lower-case page)
-         page-id (or (:db/id (db-utils/entity repo-url [:block/name page]))
-                     (:db/id (db-utils/entity repo-url [:block/original-name page])))
+   (let [page (string/lower-case (string/trim page))
+         page-entity (or (db-utils/entity repo-url [:block/name page])
+                         (db-utils/entity repo-url [:block/original-name page]))
+         page-id (:db/id page-entity)
          db (conn/get-conn repo-url)]
      (when page-id
        (some->
@@ -463,7 +529,8 @@
                                     block-eids (mapv :e datoms)]
                                 (db-utils/pull-many repo-url pull-keys block-eids)))}
                  nil)
-        react)))))
+        react
+        (flatten-blocks-sort-by-left page-entity))))))
 
 (defn get-page-blocks-no-cache
   ([page]
@@ -569,47 +636,6 @@
             rules)
            (apply concat)))))
 
-;; FIXME: alert
-(defn- keep-only-one-file
-  [blocks]
-  (filter (fn [b] (= (:block/file b) (:block/file (first blocks)))) blocks))
-
-(defn sort-by-left
-  [blocks parent]
-  (let [blocks (keep-only-one-file blocks)]
-    (when (not= (count blocks) (count (set (map :block/left blocks))))
-      (let [duplicates (->> (map (comp :db/id :block/left) blocks)
-                            frequencies
-                            (filter (fn [[_k v]] (> v 1)))
-                            (map (fn [[k _v]]
-                                   (let [left (db-utils/pull k)]
-                                     {:left left
-                                      :duplicates (->>
-                                                   (filter (fn [block]
-                                                             (= k (:db/id (:block/left block))))
-                                                           blocks)
-                                                   (map #(select-keys % [:db/id :block/level :block/content :block/file])))}))))]
-        (util/pprint duplicates)))
-    (assert (= (count blocks) (count (set (map :block/left blocks)))) "Each block should have a different left node")
-    (let [left->blocks (reduce (fn [acc b] (assoc acc (:db/id (:block/left b)) b)) {} blocks)]
-      (loop [block parent
-             result []]
-        (if-let [next (get left->blocks (:db/id block))]
-          (recur next (conj result next))
-          (vec result))))))
-
-(defn- sort-by-left-recursive
-  [form]
-  (walk/postwalk (fn [f]
-                   (if (and (map? f)
-                            (:block/_parent f))
-                     (let [children (:block/_parent f)]
-                       (-> f
-                           (dissoc :block/_parent)
-                           (assoc :block/children (sort-by-left children f))))
-                     f))
-                 form))
-
 (defn get-block-immediate-children
   "Doesn't include nested children."
   [repo block-uuid]
@@ -710,6 +736,7 @@
           :in $ ?path
           :where
           [?file :file/path ?path]
+          [?page :block/name]
           [?page :block/file ?file]]
         conn file-path)
        db-utils/seq-flatten

+ 10 - 9
src/main/frontend/db/query_dsl.cljs

@@ -6,6 +6,7 @@
             [clojure.string :as string]
             [frontend.text :as text]
             [frontend.db.query-react :as react]
+            [frontend.db.model :as model]
             [frontend.date :as date]
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
@@ -50,15 +51,15 @@
   [where blocks?]
   (when where
     (let [q (if blocks?                   ; FIXME: it doesn't need to be either blocks or pages
-             '[:find (pull ?b [*])
-               :where]
-               '[:find (pull ?p [*])
-                 :where])
-         result (if (coll? (first where))
-                  (apply conj q where)
-                  (conj q where))]
-     (prn "Datascript query: " result)
-     result)))
+              `[:find (~'pull ~'?b ~model/block-attrs)
+                :where]
+              '[:find (pull ?p [*])
+                :where])
+          result (if (coll? (first where))
+                   (apply conj q where)
+                   (conj q where))]
+      (prn "Datascript query: " result)
+      result)))
 
 ;; (between -7d +7d)
 (defn- ->journal-day-int [input]

+ 13 - 4
src/main/frontend/db/query_react.cljs

@@ -49,8 +49,17 @@
     :else
     input))
 
+(defn- remove-nested-children-blocks
+  [blocks]
+  (let [ids (set (map :db/id blocks))]
+    (->> blocks
+         (remove
+          (fn [block]
+            (let [id (:db/id (:block/parent block))]
+              (contains? ids id)))))))
+
 (defn custom-query-result-transform
-  [query-result remove-blocks q not-grouped-by-page?]
+  [query-result remove-blocks q]
   (try
     (let [repo (state/get-current-repo)
           result (db-utils/seq-flatten query-result)
@@ -63,6 +72,8 @@
                                               result))
                                     result)]
                        (some->> result
+                                remove-nested-children-blocks
+                                (model/sort-by-left-recursive)
                                 (db-utils/with-repo repo)
                                 (model/with-block-refs-count repo)
                                 (model/with-pages)))
@@ -75,9 +86,7 @@
                 (log/error :sci/call-error e)
                 result))
             result)
-          (if (and block? (not not-grouped-by-page?))
-            (db-utils/group-by-page result)
-            result))))
+          result)))
     (catch js/Error e
       (log/error :query/failed e))))
 

+ 1 - 0
src/main/frontend/dicts.cljs

@@ -252,6 +252,7 @@
         :new-graph "Add new graph"
         :graph "Graph"
         :graph-view "View Graph"
+        :cards-view "View Cards"
         :publishing "Publishing"
         :export "Export"
         :export-markdown "Export as standard Markdown (no block properties)"

+ 25 - 19
src/main/frontend/diff.cljs

@@ -4,7 +4,8 @@
             ["diff-match-patch" :as diff-match-patch]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [cljs-bean.core :as bean]))
+            [cljs-bean.core :as bean]
+            [frontend.util :as util]))
 
 ;; TODO: replace with diff-match-patch
 (defn diff
@@ -33,24 +34,29 @@
 (defn find-position
   [markup text]
   (try
-    (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))))
+    (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))))

+ 6 - 4
src/main/frontend/extensions/calc.cljc

@@ -79,7 +79,8 @@
   {:pre [(string? s)]}
   (let [env (new-env)]
     (mapv (fn [line]
-            (eval env (parse line)))
+            (when-not (str/blank? line)
+              (eval env (parse line))))
           (str/split-lines s))))
 
 ;; ======================================================================
@@ -97,6 +98,7 @@
         ;; TODO: add react keys
         (for [[i line] (map-indexed vector output-lines)]
           [:div.extensions__code-calc-output-line {:key i}
-           [:span (if (or (nil? line) (failure? line))
-                    "?"
-                    line)]])])))
+           [:span (cond
+                    (nil? line)           ""
+                    (or  (failure? line)) "?"
+                    :else                 line)]])])))

+ 5 - 2
src/main/frontend/extensions/latex.cljs

@@ -34,8 +34,11 @@
       (loader/load
        (config/asset-uri "/static/js/katex.min.js")
        (fn []
-         (reset! *loading? false)
-         (render! state)))))
+         (loader/load
+          (config/asset-uri "/static/js/mhchem.min.js")
+          (fn []
+            (reset! *loading? false)
+            (render! state)))))))
   state)
 
 (rum/defc latex < rum/reactive

+ 636 - 0
src/main/frontend/extensions/srs.cljs

@@ -0,0 +1,636 @@
+(ns frontend.extensions.srs
+  (:require [frontend.template :as template]
+            [frontend.db.query-dsl :as query-dsl]
+            [frontend.db.query-react :as react]
+            [frontend.util :as util]
+            [frontend.util.property :as property]
+            [frontend.util.persist-var :as persist-var]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.components.block :as component-block]
+            [frontend.components.macro :as component-macro]
+            [frontend.components.svg :as svg]
+            [frontend.ui :as ui]
+            [frontend.date :as date]
+            [frontend.commands :as commands]
+            [cljs-time.core :as t]
+            [cljs-time.local :as tl]
+            [cljs-time.coerce :as tc]
+            [clojure.string :as string]
+            [rum.core :as rum]
+            [frontend.modules.shortcut.core :as shortcut]))
+
+;;; ================================================================
+;;; Commentary
+;;; - One block with tag "#card" or "[[card]]" is treated as a card.
+;;; - {{cloze content}} show as "[...]" when reviewing cards
+
+;;; ================================================================
+;;; const & vars
+
+(def card-hash-tag "card")
+
+(def card-last-interval-property        :card-last-interval)
+(def card-repeats-property              :card-repeats)
+(def card-last-reviewed-property        :card-last-reviewed)
+(def card-next-schedule-property        :card-next-schedule)
+(def card-last-easiness-factor-property :card-ease-factor)
+(def card-last-score-property           :card-last-score)
+
+(def default-card-properties-map {card-last-interval-property -1
+                                  card-repeats-property 0
+                                  card-last-easiness-factor-property 2.5})
+
+(def cloze-macro-name
+  "cloze syntax: {{cloze: ...}}"
+  "cloze")
+
+(def query-macro-name
+  "{{cards ...}}"
+  "cards")
+
+(def learning-fraction-default
+  "any number between 0 and 1 (the greater it is the faster the changes of the OF matrix)"
+  0.5)
+
+(defn- learning-fraction []
+  (if-let [learning-fraction (:srs/learning-fraction (state/get-config))]
+    (if (and (number? learning-fraction)
+             (< learning-fraction 1)
+             (> learning-fraction 0))
+      learning-fraction
+      learning-fraction-default)
+    learning-fraction-default))
+
+(def of-matrix (persist-var/persist-var nil "srs-of-matrix"))
+
+(def initial-interval-default 4)
+
+(defn- initial-interval []
+  (if-let [initial-interval (:srs/initial-interval (state/get-config))]
+    (if (and (number? initial-interval)
+             (> initial-interval 0))
+      initial-interval
+      initial-interval-default)
+    initial-interval-default))
+
+;;; ================================================================
+;;; utils
+
+(defn- get-block-card-properties
+  [block]
+  (when-let [properties (:block/properties block)]
+    (merge
+     default-card-properties-map
+     (select-keys properties  [card-last-interval-property
+                               card-repeats-property
+                               card-last-reviewed-property
+                               card-next-schedule-property
+                               card-last-easiness-factor-property
+                               card-last-score-property]))))
+
+(defn- save-block-card-properties!
+  [block props]
+  (editor-handler/save-block-if-changed!
+   block
+   (property/insert-properties (:block/format block) (:block/content block) props)
+   {:force? true}))
+
+(defn- reset-block-card-properties!
+  [block]
+  (save-block-card-properties! block {card-last-interval-property -1
+                                      card-repeats-property 0
+                                      card-last-easiness-factor-property 2.5
+                                      card-last-reviewed-property "nil"
+                                      card-next-schedule-property "nil"
+                                      card-last-score-property "nil"}))
+
+
+;;; used by other ns
+
+
+(defn card-block?
+  [block]
+  (let [card-entity (db/entity [:block/name card-hash-tag])
+        refs (into #{} (:block/refs block))]
+    (contains? refs card-entity)))
+
+(declare get-root-block)
+(defn- card-group-by-repeat [cards]
+  (let [groups (group-by
+                #(get (get-block-card-properties (get-root-block %)) card-repeats-property)
+                cards)]
+    groups))
+
+;;; ================================================================
+;;; sr algorithm (sm-5)
+;;; https://www.supermemo.com/zh/archives1990-2015/english/ol/sm5
+
+(defn- fix-2f
+  [n]
+  (/ (Math/round (* 100 n)) 100))
+
+(defn- get-of [of-matrix n ef]
+  (or (get-in of-matrix [n ef])
+      (if (<= n 1)
+        (initial-interval)
+        ef)))
+
+(defn- set-of [of-matrix n ef of]
+  (->>
+   (fix-2f of)
+   (assoc-in of-matrix [n ef])))
+
+(defn- interval
+  [n ef of-matrix]
+  (if (<= n 1)
+    (get-of of-matrix 1 ef)
+    (* (get-of of-matrix n ef)
+       (interval (- n 1) ef of-matrix))))
+
+(defn- next-ef
+  [ef quality]
+  (let [ef* (+ ef (- 0.1 (* (- 5 quality) (+ 0.08 (* 0.02 (- 5 quality))))))]
+    (if (< ef* 1.3) 1.3 ef*)))
+
+(defn- next-of-matrix
+  [of-matrix n quality fraction ef]
+  (let [of (get-of of-matrix n ef)
+        of* (* of (+ 0.72 (* quality 0.07)))
+        of** (+ (* (- 1 fraction) of) (* of* fraction))]
+    (set-of of-matrix n ef of**)))
+
+(defn next-interval
+  "return [next-interval repeats next-ef of-matrix]"
+  [last-interval repeats ef quality of-matrix]
+  (assert (and (<= quality 5) (>= quality 0)))
+  (let [ef (or ef 2.5)
+        last-interval (if (or (nil? last-interval) (<= last-interval 0)) 1 last-interval)
+        next-ef (next-ef ef quality)
+        next-of-matrix (next-of-matrix of-matrix repeats quality (learning-fraction) ef)
+        next-interval (interval repeats next-ef next-of-matrix)]
+
+    (if (< quality 3)
+      ;; If the quality response was lower than 3
+      ;; then start repetitions for the item from
+      ;; the beginning without changing the E-Factor
+      [-1 1 ef next-of-matrix]
+      [(fix-2f next-interval) (+ 1 repeats) (fix-2f next-ef) next-of-matrix])))
+
+
+;;; ================================================================
+;;; card protocol
+
+
+(defprotocol ICard
+  (get-root-block [this]))
+
+(defprotocol ICardShow
+  ;; return {:value blocks :next-phase next-phase}
+  (show-cycle [this phase])
+
+  (show-cycle-config [this phase]))
+
+
+(defn- has-cloze?
+  [blocks]
+  (->> (map :block/content blocks)
+       (some #(string/includes? % "{{cloze "))))
+
+(defn- clear-collapsed-property
+  "Clear block's collapsed property if exists"
+  [blocks]
+  (let [result (map (fn [block] (assoc-in block [:block/properties :collapsed] false)) blocks)]
+    (def result result)
+    result))
+
+;;; ================================================================
+;;; card impl
+
+(deftype Sided-Cloze-Card [block]
+  ICard
+  (get-root-block [this] (db/pull [:block/uuid (:block/uuid block)]))
+  ICardShow
+  (show-cycle [this phase]
+    (let [blocks (-> (db/get-block-and-children (state/get-current-repo) (:block/uuid block))
+                     clear-collapsed-property)
+          cloze? (has-cloze? blocks)]
+      (def blocks blocks)
+      (case phase
+        1
+        (let [blocks-count (count blocks)]
+          {:value [block] :next-phase (if (> blocks-count 1) 2 3)})
+        2
+        {:value blocks :next-phase (if cloze? 3 1)}
+        3
+        {:value blocks :next-phase 1})))
+
+  (show-cycle-config [this phase]
+    (case phase
+      1
+      {}
+      2
+      {}
+      3
+      {:show-cloze? true})))
+
+(defn- ->card [block]
+  {:pre [(map? block)]}
+  (->Sided-Cloze-Card block))
+
+;;; ================================================================
+;;;
+
+(defn- query
+  "Use same syntax as frontend.db.query-dsl.
+  Add an extra condition: block's :block/refs contains `#card or [[card]]'"
+  [repo query-string]
+  (when (string? query-string)
+    (let [query-string (template/resolve-dynamic-template! query-string)]
+      (let [{:keys [query sort-by] :as result} (query-dsl/parse repo query-string)]
+        (let [query* (concat [['?b :block/refs [:block/name card-hash-tag]]]
+                             (if (coll? (first query))
+                               query
+                               [query]))]
+          (when-let [query** (query-dsl/query-wrapper query* true)]
+            (react/react-query repo
+                               {:query query**}
+                               (if sort-by
+                                 {:transform-fn sort-by}))))))))
+
+(defn- query-scheduled
+  "Return blocks scheduled to 'time' or before"
+  [repo {query-string :query-string query-result :query-result} time]
+  (when-let [*blocks (or query-result (and query-string (query repo query-string)))]
+    (when-let [blocks @*blocks]
+      (let [filtered-result (->>
+                             (flatten blocks)
+                             (filterv (fn [b]
+                                        (let [props (:block/properties b)
+                                              next-sched (get props card-next-schedule-property)
+                                              next-sched* (tc/from-string next-sched)
+                                              repeats (get props card-repeats-property)]
+                                          (or (nil? repeats)
+                                              (< repeats 1)
+                                              (nil? next-sched)
+                                              (nil? next-sched*)
+                                              (t/before? next-sched* time))))))]
+        {:total (count blocks)
+         :result filtered-result}))))
+
+
+;;; ================================================================
+;;; operations
+
+
+(defn- get-next-interval
+  [card score]
+  {:pre [(and (<= score 5) (>= score 0))
+         (satisfies? ICard card)]}
+  (let [block (.-block card)
+        props (get-block-card-properties block)
+        last-interval (or (util/safe-parse-float (get props card-last-interval-property)) 0)
+        repeats (or (util/safe-parse-int (get props card-repeats-property)) 0)
+        last-ef (or (util/safe-parse-float (get props card-last-easiness-factor-property)) 2.5)]
+    (let [[next-interval next-repeats next-ef of-matrix*]
+          (next-interval last-interval repeats last-ef score @of-matrix)
+          next-interval* (if (< next-interval 0) 0 next-interval)
+          next-schedule (tc/to-string (t/plus (tl/local-now) (t/hours (* 24 next-interval*))))
+          now (tc/to-string (tl/local-now))]
+      {:next-of-matrix of-matrix*
+       card-last-interval-property next-interval
+       card-repeats-property next-repeats
+       card-last-easiness-factor-property next-ef
+       card-next-schedule-property next-schedule
+       card-last-reviewed-property now
+       card-last-score-property score})))
+
+(defn- operation-score!
+  [card score]
+  {:pre [(and (<= score 5) (>= score 0))
+         (satisfies? ICard card)]}
+  (let [block (.-block card)
+        result (get-next-interval card score)
+        next-of-matrix (:next-of-matrix result)]
+    (reset! of-matrix next-of-matrix)
+    (save-block-card-properties! (db/pull [:block/uuid (:block/uuid block)])
+                                 (select-keys result
+                                              [card-last-interval-property
+                                               card-repeats-property
+                                               card-last-easiness-factor-property
+                                               card-next-schedule-property
+                                               card-last-reviewed-property
+                                               card-last-score-property]))))
+
+(defn- operation-reset!
+  [card]
+  {:pre [(satisfies? ICard card)]}
+  (let [block (.-block card)]
+    (reset-block-card-properties! (db/pull [:block/uuid (:block/uuid block)]))))
+
+(defn- operation-card-info-summary!
+  [review-records review-cards card-query-block]
+  (when card-query-block
+    (let [review-count (count (flatten (vals review-records)))
+          review-cards-count (count review-cards)
+          score-5-count (count (get review-records 5))
+          score-4-count (count (get review-records 4))
+          score-3-count (count (get review-records 3))
+          score-2-count (count (get review-records 2))
+          score-1-count (count (get review-records 1))
+          score-0-count (count (get review-records 0))
+          skip-count (count (get review-records "skip"))]
+      (editor-handler/paste-block-tree-after-target
+       (:db/id card-query-block) false
+       [{:content (util/format "Summary: %d items, %d review counts [[%s]]"
+                               review-cards-count review-count (date/today))
+         :children [{:content
+                     (util/format "Remembered:   %d (%d%%)" score-5-count (* 100 (/ score-5-count review-count)))}
+                    {:content
+                     (util/format "Forgotten :   %d (%d%%)" score-1-count (* 100 (/ score-1-count review-count)))}]}]
+       (:block/format card-query-block)))))
+
+;;; ================================================================
+;;; UI
+
+(defn- score-help-info [days-3 days-4 days-5]
+  (ui/tippy {:html [:div
+                    [:p.text-sm "0-2: you have forgotten this card."]
+                    [:p.text-sm "3-5: you remember this card."]
+                    [:p.text-sm "0: completely forgot."]
+                    [:p.text-sm "1: it still takes a while to recall even after seeing the answer."]
+                    [:p.text-sm "2: immediately recall after seeing the answer."]
+                    [:p.text-sm
+                     (util/format "3: it takes a while to recall. (will reappear after %d days)" days-3)]
+                    [:p.text-sm
+                     (util/format "4: you recall this after a little thought. (will reappear after %d days)"
+                                  days-4)]
+                    [:p.text-sm
+                     (util/format "5: you remember it easily. (will reappear after %d days)" days-5)]]
+             :class "tippy-hover"
+             :interactive true
+             :disabled false}
+            (svg/info)))
+
+(defn- score-and-next-card [score card *card-index *cards *phase *review-records cb]
+  (operation-score! card score)
+  (swap! *review-records #(update % score (fn [ov] (conj ov card))))
+  (if (>= (inc @*card-index) (count @*cards))
+    (when cb
+      (swap! *card-index inc)
+      (cb @*review-records))
+    (do
+      (swap! *card-index inc)
+      (reset! *phase 1))))
+
+(defn- skip-card [card *card-index *cards *phase *review-records cb]
+  (swap! *review-records #(update % "skip" (fn [ov] (conj ov card))))
+  (swap! *card-index inc)
+  (if (>= (inc @*card-index) (count @*cards))
+    (and cb (cb @*review-records))
+    (reset! *phase 1)))
+
+(def review-finished
+  [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
+
+(rum/defcs view
+  < rum/reactive
+  (rum/local 1 ::phase)
+  (rum/local 0 ::card-index)
+  (rum/local nil ::cards)
+  (rum/local {} ::review-records)
+  [state cards {preview? :preview?
+                modal? :modal?
+                cb :callback}]
+  (let [cards* (::cards state)
+        _ (when (nil? @cards*) (reset! cards* cards))
+        review-records (::review-records state)
+        card-index (::card-index state)
+        card (util/nth-safe @cards* @card-index)]
+    (if-not card
+      review-finished
+      (let [phase (::phase state)
+            {blocks :value next-phase :next-phase} (show-cycle card @phase)
+            root-block (.-block card)
+            root-block-id (:block/uuid root-block)]
+        [:div.ls-card
+         {:class (if (or preview? modal?)
+                   (util/hiccup->class ".flex.flex-col.resize.overflow-y-auto.px-4"))}
+         (component-block/blocks-container
+          blocks
+          (merge (show-cycle-config card @phase)
+                 {:id (str root-block-id)}))
+         (if (or preview? modal?)
+           [:div.flex.my-4.justify-between
+            [:div.flex-1
+             (when-not (and (not preview?) (= next-phase 1))
+               (ui/button (case next-phase
+                            1 "Hide answers(s)"
+                            2 "Show Answers(s)"
+                            3 "Show clozes(s)")
+                 :id "card-answers"
+                 :class "mr-2"
+                 :small? true
+                 :on-click #(reset! phase next-phase)))
+
+             (when (and (> (count cards) 1) preview?)
+               (ui/button "Next(n)"
+                 :id "card-next"
+                 :small? true
+                 :class "mr-2"
+                 :on-click #(skip-card card card-index cards* phase review-records cb)))
+
+             (when (and (not preview?) (= 1 next-phase))
+               (let [interval-days-score-3 (get (get-next-interval card 3) card-last-interval-property)
+                     interval-days-score-4 (get (get-next-interval card 5) card-last-interval-property)
+                     interval-days-score-5 (get (get-next-interval card 5) card-last-interval-property)]
+                 [:div.flex.flex-row.justify-between
+                  (ui/button "Forgotten(f)"
+                    :id "card-forgotten"
+                    :small? true
+                    :on-click (fn []
+                                (score-and-next-card 1 card card-index cards* phase review-records cb)
+                                (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
+                                  (editor-handler/set-block-property! root-block-id card-next-schedule-property tomorrow))))
+
+                  (ui/button "Remembered(r)"
+                    :id "card-remembered"
+                    :small? true
+                    :on-click #(score-and-next-card 5 card card-index cards* phase review-records cb))
+
+                  (ui/button "Take a while to recall(t)"
+                    :id "card-recall"
+                    :class (util/hiccup->class "opacity-60.hover:opacity-100")
+                    :small? true
+                    :on-click #(score-and-next-card 3 card card-index cards* phase review-records cb))]))]
+
+            (when preview?
+              (ui/tippy {:html [:div.text-sm
+                                "Reset this card so that you can review it immediately."]
+                         :class "tippy-hover"
+                         :interactive true}
+               (ui/button "Reset"
+                 :id "card-reset"
+                 :class (util/hiccup->class "opacity-60.hover:opacity-100")
+                 :small? true
+                 :on-click #(operation-reset! card))))]
+           [:div.my-4
+            (ui/button "Click to review"
+              :small? true)])]))))
+
+(rum/defc view-modal <
+  (shortcut/mixin :shortcut.handler/cards)
+  [cards option]
+  (view cards option))
+
+(defn preview
+  [blocks]
+  (state/set-modal! #(view (mapv ->card blocks) {:preview? true})))
+
+
+;;; ================================================================
+;;; register some external vars & related UI
+
+;;; register cloze macro
+
+
+(rum/defcs cloze-macro-show < rum/reactive
+  {:init (fn [state]
+           (let [shown? (atom (:show-cloze? config))]
+             (assoc state :shown? shown?)))}
+  [state config options]
+  (let [shown?* (:shown? state)
+        shown? (rum/react shown?*)
+        toggle! #(swap! shown?* not)]
+    (if (or shown? (:show-cloze? config))
+      [:a.cloze-revealed {:on-click toggle!}
+       (util/format "[%s]" (string/join ", " (:arguments options)))]
+      [:a.cloze {:on-click toggle!}
+       "[...]"])))
+
+(component-macro/register cloze-macro-name cloze-macro-show)
+
+;;; register cards macro
+(rum/defcs cards
+  < rum/reactive
+  (rum/local false ::need-requery)
+  [state config options]
+  (let [repo (state/get-current-repo)
+        query-string (string/join ", " (:arguments options))]
+    (if-let [*query-result (query repo query-string)]
+      (let [{:keys [total result]} (query-scheduled repo {:query-result *query-result} (tl/local-now))
+            review-cards (mapv ->card result)
+            query-string (if (string/blank? query-string) "All" query-string)
+            card-query-block (db/entity [:block/uuid (:block/uuid config)])
+            filtered-total (count result)
+            modal? (:modal? config)]
+        [:div.flex-1 {:style (if modal? {:height "100%"})}
+         [:div.flex.flex-row.items-center.justify-between.cards-title
+          [:div
+           [:span.text-sm [:span.font-bold "🗂️"]
+            (str ": " query-string)]]
+
+          [:div.flex.flex-row.items-center
+
+           ;; FIXME: CSS issue
+           (ui/tippy {:html [:div.text-sm "overdue/total"]
+                      ;; :class "tippy-hover"
+                      :interactive true}
+                     [:div.opacity-60.text-sm
+                      filtered-total
+                      [:span "/"]
+                      total])
+
+           (when-not modal?
+             (ui/tippy
+              {:html [:div.text-sm "Click to preview all cards"]
+               :delay [1000, 100]
+               :class "tippy-hover"
+               :interactive true
+               :disabled false}
+              [:a.opacity-60.hover:opacity-100.svg-small.inline.ml-3.font-bold
+               {:on-click (fn [_]
+                            (let [all-blocks (flatten @(query (state/get-current-repo) query-string))]
+                              (when (> (count all-blocks) 0)
+                                (let [review-cards (mapv ->card all-blocks)]
+                                  (state/set-modal! #(view-modal
+                                                      review-cards
+                                                      {:preview? true
+                                                       :callback (fn [_]
+                                                                   (swap! (::need-requery state) not))}))))))}
+               "A"]))]]
+         (if (seq review-cards)
+           [:div (when-not modal?
+                   {:on-click (fn []
+                                (state/set-modal! #(view-modal
+                                                    review-cards
+                                                    {:modal? true
+                                                     :callback
+                                                     (fn [review-records]
+                                                       (operation-card-info-summary!
+                                                        review-records review-cards card-query-block)
+                                                       (swap! (::need-requery state) not)
+                                                       (persist-var/persist-save of-matrix))})))})
+            (let [view-fn (if modal? view-modal view)]
+              (view-fn review-cards
+                       (merge config
+                              {:callback
+                               (fn [review-records]
+                                 (operation-card-info-summary!
+                                  review-records review-cards card-query-block)
+                                 (swap! (::need-requery state) not)
+                                 (persist-var/persist-save of-matrix))})))]
+           review-finished)])
+
+      (let [result (query (state/get-current-repo) "")]
+        (if (or
+             (nil? result)
+             (and result (empty? @result)))
+         [:div.ls-card
+          [:h1.title "Time to create your first card!"]
+
+          [:div
+           [:p "You can add \"#card\" to any block to turn it into a card or trigger \"/cloze\" to add some clozes."]
+           [:img.my-4 {:src "https://logseq.github.io/assets/2021-07-22_22.28.02_1626964258528_0.gif"}]
+           [:p "You can "
+            [:a {:href "https://logseq.github.io/#/page/cards" :target "_blank"}
+             "click this link"]
+            " to check the documentation."]]]
+
+         [:div.opacity-60.custom-query-title.ls-card
+          [:div.w-full.flex-1
+           [:code.p-1 (str "Cards: " query-string)]]
+          [:div.mt-2.ml-2.font-medium "No matched cards"]])))))
+
+(rum/defc global-cards
+  []
+  (cards {:modal? true} {}))
+
+(component-macro/register query-macro-name cards)
+
+;;; register builtin properties
+(property/register-built-in-properties #{card-last-interval-property
+                                         card-repeats-property
+                                         card-last-reviewed-property
+                                         card-next-schedule-property
+                                         card-last-easiness-factor-property
+                                         card-last-score-property})
+
+;;; register slash commands
+(commands/register-slash-command ["Cards"
+                                  [[:editor/input "{{cards }}" {:backward-pos 2}]]
+                                  "Create a cards query"])
+
+(commands/register-slash-command ["Cloze"
+                                  [[:editor/input "{{cloze }}" {:backward-pos 2}]]
+                                  "Create a cloze"])
+
+;; handlers
+(defn make-block-a-card!
+  [block-id]
+  (when-let [content (:block/content (db/entity [:block/uuid block-id]))]
+    (editor-handler/save-block!
+     (state/get-current-repo)
+     block-id
+     (str (string/trim content) " #" card-hash-tag))))

+ 21 - 0
src/main/frontend/extensions/srs/handler.cljs

@@ -0,0 +1,21 @@
+(ns frontend.extensions.srs.handler)
+
+(defn click
+  [id]
+  (when-let [node (js/document.getElementById id)]
+    (.click node)))
+
+(defn toggle-answers []
+  (click "card-answers"))
+
+(defn next-card []
+  (click "card-next"))
+
+(defn forgotten []
+  (click "card-forgotten"))
+
+(defn remembered []
+  (click "card-remembered"))
+
+(defn recall []
+  (click "card-recall"))

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

@@ -46,7 +46,7 @@
      (if (string/blank? content)
        ""
        (if-let [record (get-format-record format)]
-         (protocol/toHtml record content config)
+         (protocol/toHtml record content config mldoc/default-references)
          content)))))
 
 (defn to-edn

+ 1 - 1
src/main/frontend/format/adoc.cljs

@@ -13,7 +13,7 @@
   protocol/Format
   (toEdn [this content config]
     (->edn content config))
-  (toHtml [this content config]
+  (toHtml [this content config references]
     (when (loaded?)
       (let [config {:attributes {:showTitle false
                                  :hardbreaks true

+ 24 - 22
src/main/frontend/format/block.cljs

@@ -624,8 +624,7 @@
               (let [block (assoc block
                                  :block/parent parent
                                  :block/left [:block/uuid uuid]
-                                 :block/level level
-                                 )
+                                 :block/level level)
                     parents' (conj (vec (butlast parents)) block)
                     result' (conj result block)]
                 [others parents' block result'])
@@ -647,26 +646,29 @@
                     result' (conj result block)]
                 [others parents' block result'])
 
-              ;; - a
-              ;;    - b
-              ;;  - c
-              (and (>= (count parents) 2)
-                   (< level-spaces parent-spaces)
-                   (> level-spaces (:block/level-spaces (nth parents (- (count parents) 2)))))
-              (let [block (assoc block
-                                 :block/parent parent
-                                 :block/left [:block/uuid uuid]
-                                 :block/level level
-                                 :block/level-spaces parent-spaces)
-                    parents' (conj (vec (butlast parents)) block)
-                    result' (conj result block)]
-                [others parents' block result'])
-
-              (< level-spaces parent-spaces)         ; outdent
-              (let [parents' (vec (filter (fn [p] (<= (:block/level-spaces p) level-spaces)) parents))
-                    blocks (cons (assoc (first blocks) :block/level (dec level))
-                                 (rest blocks))]
-                [blocks parents' (last parents') result]))]
+              (< level-spaces parent-spaces)
+              (cond
+                (some #(= (:block/level-spaces %) (:block/level-spaces block)) parents) ; outdent
+                (let [parents' (vec (filter (fn [p] (<= (:block/level-spaces p) level-spaces)) parents))
+                      left (last parents')
+                      blocks (cons (assoc (first blocks)
+                                          :block/level (dec level)
+                                          :block/left [:block/uuid (:block/uuid left)])
+                                   (rest blocks))]
+                  [blocks parents' left result])
+
+                :else
+                (let [[f r] (split-with (fn [p] (<= (:block/level-spaces p) level-spaces)) parents)
+                      left (first r)
+                      parents' (->> (concat f [left]) vec)
+                      block (assoc block
+                                   :block/parent [:block/uuid (:block/uuid (last f))]
+                                   :block/left [:block/uuid (:block/uuid left)]
+                                   :block/level (:block/level left)
+                                   :block/level-spaces (:block/level-spaces left))
+                      parents' (->> (concat f [block]) vec)
+                      result' (conj result block)]
+                  [others parents' block result'])))]
         (recur blocks parents sibling result)))))
 
 (defn- parse-block

+ 2 - 2
src/main/frontend/format/mldoc.cljs

@@ -252,8 +252,8 @@
   protocol/Format
   (toEdn [this content config]
     (->edn content config))
-  (toHtml [this content config]
-    (exportToHtml content config))
+  (toHtml [this content config references]
+    (exportToHtml content config references))
   (loaded? [this]
     true)
   (lazyLoad [this ok-handler]

+ 1 - 1
src/main/frontend/format/protocol.cljs

@@ -2,7 +2,7 @@
 
 (defprotocol Format
   (toEdn [this content config])
-  (toHtml [this content config])
+  (toHtml [this content config references])
   (loaded? [this])
   (lazyLoad [this ok-handler])
   (exportMarkdown [this content config references])

+ 66 - 53
src/main/frontend/fs/nfs.cljs

@@ -139,61 +139,74 @@
           handle-path (string/replace handle-path "//" "/")
           basename-handle-path (str handle-path "/" basename)]
       (p/let [file-handle (idb/get-item basename-handle-path)]
-        (when file-handle
-          (add-nfs-file-handle! basename-handle-path file-handle))
-        (if file-handle
-          (p/let [local-file (.getFile file-handle)
-                  local-content (.text local-file)
-                  local-last-modified-at (gobj/get local-file "lastModified")
-                  current-time (util/time-ms)
-                  new? (> current-time local-last-modified-at)
-                  new-created? (nil? last-modified-at)
-                  not-changed? (= last-modified-at local-last-modified-at)
-                  format (-> (util/get-file-ext path)
-                             (config/get-file-format))
-                  pending-writes (state/get-write-chan-length)
-                  draw? (and path (string/ends-with? path ".excalidraw"))
-                  config? (and path (string/ends-with? path "/config.edn"))]
-            (p/let [_ (verify-permission repo file-handle true)
-                    _ (utils/writeFile file-handle content)
-                    file (.getFile file-handle)]
-              (if (and local-content new?
-                       (or
-                        draw?
-                        config?
-                        ;; Writing not finished
-                        (> pending-writes 0)
-                        ;; not changed by other editors
-                        not-changed?
-                        new-created?))
-                (p/let [_ (verify-permission repo file-handle true)
-                        _ (utils/writeFile file-handle content)
-                        file (.getFile file-handle)]
-                  (when file
-                    (nfs-saved-handler repo path file)))
-                (js/alert (str "The file has been modified on your local disk! File path: " path
-                               ", please save your changes and click the refresh button to reload it.")))))
-           ;; create file handle
-          (->
-           (p/let [handle (idb/get-item handle-path)]
-             (if handle
-               (p/let [_ (verify-permission repo handle true)
-                       file-handle (.getFileHandle ^js handle basename #js {:create true})
-                       ;; File exists if the file-handle has some content in it.
-                       file (.getFile file-handle)
-                       text (.text file)]
-                 (if (string/blank? text)
-                   (p/let [_ (idb/set-item! basename-handle-path file-handle)
+        ;; check file-handle available, remove it when got 'NotFoundError'
+        (p/let [test-get-file (when file-handle
+                                (p/catch (p/let [_ (.getFile file-handle)] true)
+                                         (fn [e]
+                                           (js/console.dir e)
+                                           (when (= "NotFoundError" (.-name e))
+                                             (idb/remove-item! basename-handle-path)
+                                             (remove-nfs-file-handle! basename-handle-path))
+                                           false)))
+                file-handle (if test-get-file file-handle nil)]
+
+          (when file-handle
+            (add-nfs-file-handle! basename-handle-path file-handle))
+          (if file-handle
+            (-> (p/let [local-file (.getFile file-handle)
+                        local-content (.text local-file)
+                        local-last-modified-at (gobj/get local-file "lastModified")
+                        current-time (util/time-ms)
+                        new? (> current-time local-last-modified-at)
+                        new-created? (nil? last-modified-at)
+                        not-changed? (= last-modified-at local-last-modified-at)
+                        format (-> (util/get-file-ext path)
+                                   (config/get-file-format))
+                        pending-writes (state/get-write-chan-length)
+                        draw? (and path (string/ends-with? path ".excalidraw"))
+                        config? (and path (string/ends-with? path "/config.edn"))]
+                  (p/let [_ (verify-permission repo file-handle true)
                           _ (utils/writeFile file-handle content)
                           file (.getFile file-handle)]
-                    (when file
-                      (nfs-saved-handler repo path file)))
-                   (notification/show! (str "The file " path " already exists, please save your changes and click the refresh button to reload it.")
-                    :warning)))
-               (println "Error: directory handle not exists: " handle-path)))
-           (p/catch (fn [error]
-                      (println "Write local file failed: " {:path path})
-                      (js/console.error error))))))))
+                    (if (and local-content new?
+                             (or
+                              draw?
+                              config?
+                             ;; Writing not finished
+                              (> pending-writes 0)
+                             ;; not changed by other editors
+                              not-changed?
+                              new-created?))
+                      (p/let [_ (verify-permission repo file-handle true)
+                              _ (utils/writeFile file-handle content)
+                              file (.getFile file-handle)]
+                        (when file
+                          (nfs-saved-handler repo path file)))
+                      (js/alert (str "The file has been modified on your local disk! File path: " path
+                                     ", please save your changes and click the refresh button to reload it.")))))
+                (p/catch (fn [e]
+                           (js/console.error e))))
+            ;; create file handle
+            (->
+             (p/let [handle (idb/get-item handle-path)]
+               (if handle
+                 (p/let [_ (verify-permission repo handle true)
+                         file-handle (.getFileHandle ^js handle basename #js {:create true})
+                         ;; File exists if the file-handle has some content in it.
+                         file (.getFile file-handle)
+                         text (.text file)]
+                   (if (string/blank? text)
+                     (p/let [_ (idb/set-item! basename-handle-path file-handle)
+                             _ (utils/writeFile file-handle content)
+                             file (.getFile file-handle)]
+                       (when file
+                         (nfs-saved-handler repo path file)))
+                     (notification/show! (str "The file " path " already exists, please save your changes and click the refresh button to reload it.")
+                                         :warning)))
+                 (println "Error: directory handle not exists: " handle-path)))
+             (p/catch (fn [error]
+                        (println "Write local file failed: " {:path path})
+                        (js/console.error error)))))))))
 
   (rename! [this repo old-path new-path]
     (p/let [[dir basename] (util/get-dir-and-basename old-path)

+ 35 - 20
src/main/frontend/fs/node.cljs

@@ -8,7 +8,8 @@
             [cljs-bean.core :as bean]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [frontend.config :as config]))
+            [frontend.config :as config]
+            [frontend.handler.notification :as notification]))
 
 (defn concat-path
   [dir path]
@@ -28,28 +29,42 @@
   [this repo dir path content {:keys [ok-handler error-handler skip-mtime?] :as opts} stat]
   (if skip-mtime?
     (p/catch
-     (p/let [result (ipc/ipc "writeFile" path content)]
-       (when ok-handler
-         (ok-handler repo path result)))
-     (fn [error]
-       (if error-handler
-         (error-handler error)
-         (log/error :write-file-failed error))))
+        (p/let [result (ipc/ipc "writeFile" path content)]
+          (when ok-handler
+            (ok-handler repo path result)))
+        (fn [error]
+          (if error-handler
+            (error-handler error)
+            (log/error :write-file-failed error))))
 
     (p/let [disk-mtime (when stat (gobj/get stat "mtime"))
             db-mtime (db/get-file-last-modified-at repo path)
-            old-content nil
-            old-content (-> (protocol/read-file this dir path nil)
-                            (p/catch (fn [error]
-                                       "")))
-            ext (string/lower-case (util/get-file-ext path))]
-      (if (and
-           (and (not= disk-mtime db-mtime)
-                (not= (string/trim old-content) (string/trim content)))
-            ;; FIXME:
-           (not (contains? #{"excalidraw" "edn"} ext)))
-        (js/alert (str "The file has been modified on your local disk! File path: " path
-                       ", please save your changes and click the refresh button to reload it."))
+            disk-content (-> (protocol/read-file this dir path nil)
+                             (p/catch (fn [error] nil)))
+            disk-content (or disk-content "")
+            ext (string/lower-case (util/get-file-ext path))
+            file-page (db/get-file-page-id path)
+            page-empty? (and file-page (db/page-empty? repo file-page))]
+      (cond
+        ;; (and (not page-empty?) (nil? disk-content) )
+        ;; (notification/show!
+        ;;  (str "The file has been renamed or deleted on your local disk! File path: " path
+        ;;       ", please save your changes and click the refresh button to reload it.")
+        ;;  :error
+        ;;  false)
+
+        (and
+         (not= disk-mtime db-mtime)
+         (not= (string/trim disk-content) (string/trim content))
+         ;; FIXME:
+         (not (contains? #{"excalidraw" "edn"} ext)))
+        (notification/show!
+         (str "The file has been modified on your local disk! File path: " path
+              ", please save your changes and click the refresh button to reload it.")
+         :warning
+         false)
+
+        :else
         (->
          (p/let [result (ipc/ipc "writeFile" path content)
                  mtime (gobj/get result "mtime")]

+ 0 - 0
src/main/frontend/handler/card.cljs


+ 25 - 17
src/main/frontend/handler/editor.cljs

@@ -45,7 +45,8 @@
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [medley.core :as medley]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            ["/frontend/utils" :as utils]))
 
 ;; FIXME: should support multiple images concurrently uploading
 
@@ -183,7 +184,7 @@
                     "edit-block")))
 
 (defn clear-selection!
-  [_e]
+  []
   (util/select-unhighlight! (dom/by-class "selected"))
   (state/clear-selection!))
 
@@ -228,7 +229,7 @@
                           (subs content 0 pos))
              content (property/remove-built-in-properties (:block/format block)
                                                           content)]
-         (clear-selection! nil)
+         (clear-selection!)
          (state/set-editing! edit-input-id content block text-range move-cursor?))))))
 
 (defn edit-last-block-for-new-page!
@@ -324,7 +325,7 @@
         block (update block :block/refs remove-non-existed-refs!)
         block (attach-page-properties-if-exists! block)
         new-properties (merge
-                        (select-keys properties property/built-in-properties)
+                        (select-keys properties (property/built-in-properties))
                         (:block/properties block))]
     (-> block
         (dissoc :block/top?
@@ -1495,15 +1496,19 @@
    "`" "`"
    "~" "~"
    "*" "*"
-   ;; "_" "_"
+   "_" "_"
+   "^" "^"
    ;; ":" ":"                              ; TODO: only properties editing and org mode tag
-   ;; "^" "^"
+
    })
 
 (def reversed-autopair-map
   (zipmap (vals autopair-map)
           (keys autopair-map)))
 
+(defonce autopair-when-selected
+  #{"^" "_"})
+
 (def delete-map
   (assoc autopair-map
          "$" "$"
@@ -2047,7 +2052,7 @@
             (recur (zip/next loc))
             (let [content (:content node)
                   props (into [] (:properties node))
-                  content* (str "- "
+                  content* (str (if (= :markdown format) "- " "* ")
                                 (property/insert-properties format content props))
                   ast (mldoc/->edn content* (mldoc/default-config format))
                   blocks (block/extract-blocks ast content* true format)
@@ -2128,8 +2133,7 @@
   [state]
   (when-not (auto-complete?)
     (let [{:keys [block config]} (get-state)]
-      (when (and block
-                 (not (:custom-query? config)))
+      (when block
         (let [content (state/get-edit-content)
               current-node (outliner-core/block block)
               has-right? (-> (tree/-get-right current-node)
@@ -2475,6 +2479,9 @@
           (util/stop e)
           (cursor/move-cursor-forward input))
 
+        (and (autopair-when-selected key) (string/blank? (util/get-selected-text)))
+        nil
+
         (contains? (set (keys autopair-map)) key)
         (do
           (util/stop e)
@@ -2658,7 +2665,8 @@
     (if (and
          (:copy/content copied-blocks)
          (not (string/blank? text))
-         (= (string/trim text) (string/trim (:copy/content copied-blocks))))
+         (= (string/replace (string/trim text) "\r" "")
+            (string/replace (string/trim (:copy/content copied-blocks)) "\r" "")))
       (do
         ;; copy from logseq internally
         (paste-block-vec-tree-at-target copied-block-tree [])
@@ -2735,8 +2743,7 @@
 (defn- cut-blocks-and-clear-selections!
   [copy?]
   (cut-selection-blocks copy?)
-  (clear-selection! nil))
-
+  (clear-selection!))
 (defn shortcut-copy-selection
   [e]
   (copy-selection-blocks))
@@ -2956,7 +2963,7 @@
                       medley/uuid
                       expand-block!)))
            doall)
-      (clear-selection! nil))
+      (clear-selection!))
 
     :else
     ;; expand one level
@@ -2990,7 +2997,7 @@
                       medley/uuid
                       collapse-block!)))
            doall)
-      (clear-selection! nil))
+      (clear-selection!))
 
     :else
     ;; collapse by one level from outside
@@ -3073,8 +3080,9 @@
 
 (defn paste-text-in-one-block-at-point
   []
-  (.then
-   (js/navigator.clipboard.readText)
+  (utils/getClipText
    (fn [clipboard-data]
      (when-let [_ (state/get-input)]
-       (state/append-current-edit-content! clipboard-data)))))
+       (state/append-current-edit-content! clipboard-data)))
+   (fn [error]
+     (js/console.error error))))

+ 4 - 0
src/main/frontend/handler/events.cljs

@@ -13,6 +13,7 @@
             [frontend.fs.nfs :as nfs]
             [frontend.db.conn :as conn]
             [frontend.handler.migrate :as migrate]
+            [frontend.extensions.srs :as srs]
             [frontend.db-schema :as db-schema]
             [frontend.db :as db]
             [datascript.core :as d]
@@ -141,6 +142,9 @@
         shown-properties (set/intersection (set all-properties) shown-properties)]
     (state/set-modal! (query-properties-settings block shown-properties all-properties))))
 
+(defmethod handle :modal/show-cards [_]
+  (state/set-modal! srs/global-cards))
+
 (defmethod handle :after-db-restore [[_ repos]]
   (mapv (fn [{url :url} repo]
           ;; compare :ast/version

+ 22 - 26
src/main/frontend/handler/export.cljs

@@ -67,23 +67,6 @@
    (outliner-tree/blocks->vec-tree (str (:block/uuid block)))
    (outliner-file/tree->file-content {:init-level 1})))
 
-(defn copy-block-as-json!
-  [block-id]
-  (when-let [repo (state/get-current-repo)]
-    (let [block-children (db/get-block-and-children repo block-id)]
-      (util/copy-to-clipboard! (js/JSON.stringify (bean/->js block-children))))))
-
-(defn copy-page-as-json!
-  [page-name]
-  (when-let [repo (state/get-current-repo)]
-    (let [properties (db/get-page-properties page-name)
-          blocks (db/get-page-blocks repo page-name)]
-      (util/copy-to-clipboard!
-       (js/JSON.stringify
-        (bean/->js
-         {:properties properties
-          :blocks blocks}))))))
-
 (defn export-repo-as-json!
   [repo]
   (when-let [db (db/get-conn repo)]
@@ -184,7 +167,7 @@
 
 (defn- get-embed-and-refs-blocks-pages-aux []
   (let [mem (atom {})]
-    (letfn [(f [repo page-or-block is-block? exclude-blocks exclude-pages]
+    (letfn [(f [repo page-or-block is-block? exclude-blocks exclude-pages ttl]
               (let [v (get @mem [repo page-or-block])]
                 (if v v
                     (let [[ref-blocks ref-pages]
@@ -218,13 +201,15 @@
                                (filterv :block/name)
                                (flatten))
                           [next-ref-blocks1 next-ref-pages1]
-                          (->> ref-blocks
-                               (mapv #(f repo % true (set (concat ref-block-ids exclude-blocks)) exclude-pages))
-                               (apply mapv vector))
+                          (if (<= ttl 0) [[] []]
+                              (->> ref-blocks
+                                   (mapv #(f repo % true (set (concat ref-block-ids exclude-blocks)) exclude-pages (- ttl 1)))
+                                   (apply mapv vector)))
                           [next-ref-blocks2 next-ref-pages2]
-                          (->> ref-pages
-                               (mapv #(f repo (:block/name %) false exclude-blocks (set (concat ref-page-ids exclude-pages))))
-                               (apply mapv vector))
+                          (if (<= ttl 0) [[] []]
+                              (->> ref-pages
+                                   (mapv #(f repo (:block/name %) false exclude-blocks (set (concat ref-page-ids exclude-pages)) (- ttl 1)))
+                                   (apply mapv vector)))
                           result
                           [(->> (concat ref-block-ids next-ref-blocks1 next-ref-blocks2)
                                 (flatten)
@@ -240,7 +225,7 @@
 (defn- get-page&block-refs-by-query
   [repo page-or-block get-page&block-refs-by-query-aux {:keys [is-block?] :or {is-block? false}}]
   (let [[block-ids page-ids]
-        (get-page&block-refs-by-query-aux repo page-or-block is-block? #{} #{})
+        (get-page&block-refs-by-query-aux repo page-or-block is-block? #{} #{} 3)
         blocks
         (db/pull-many repo '[*] block-ids)
         pages-name-and-content
@@ -475,7 +460,18 @@
                        (f/get-default-config format {:export-md-indent-style indent-style})
                        (js/JSON.stringify (clj->js refs)))))
 
-
+(defn export-blocks-as-html
+  [repo root-block-uuid]
+  (let [get-page&block-refs-by-query-aux (get-embed-and-refs-blocks-pages-aux)
+        f #(get-page&block-refs-by-query repo % get-page&block-refs-by-query-aux {:is-block? true})
+        root-block (db/entity [:block/uuid root-block-uuid])
+        blocks (db/get-block-and-children repo root-block-uuid)
+        refs (f blocks)
+        content (get-blocks-contents repo root-block-uuid)
+        format (or (:block/format root-block) (state/get-preferred-format))]
+    (fp/toHtml f/mldoc-record content
+                       (f/get-default-config format)
+                       (js/JSON.stringify (clj->js refs)))))
 
 (defn- convert-md-files-unordered-list-or-heading
   [repo files heading-to-list?]

+ 1 - 2
src/main/frontend/handler/extract.cljs

@@ -43,8 +43,7 @@
                                     (string? title)
                                     title))
             file-name (when-let [file-name (last (string/split file #"/"))]
-                        (-> (first (util/split-last "." file-name))
-                            (string/replace "." "/")))]
+                        (first (util/split-last "." file-name)))]
         (or property-name
             (if (= (state/page-name-order) "heading")
               (or first-block-name file-name)

+ 1 - 2
src/main/frontend/handler/page.cljs

@@ -454,8 +454,7 @@
   (->> (db/get-all-pages)
        (remove (fn [p]
                  (let [name (:block/name p)]
-                   (or (util/file-page? name)
-                       (util/uuid-string? name)
+                   (or (util/uuid-string? name)
                        (db/built-in-pages-names (string/upper-case name))))))
        (common-handler/fix-pages-timestamps)))
 

+ 6 - 0
src/main/frontend/handler/ui.cljs

@@ -210,3 +210,9 @@
   (when-not (input-or-select?)
     (util/stop e)
     (swap! *internal-model inc-week 1)))
+
+(defn toggle-cards!
+  []
+  (if (:modal/show? @state/state)
+    (state/close-modal!)
+    (state/pub-event! [:modal/show-cards])))

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

@@ -26,8 +26,11 @@
 (defn remove-ignore-files
   [files]
   (let [files (remove (fn [f]
-                        (or (string/starts-with? (:file/path f) ".git/")
-                            (string/includes? (:file/path f) ".git/")))
+                        (let [path (:file/path f)]
+                          (or (string/starts-with? path ".git/")
+                              (string/includes? path ".git/")
+                              (and (util/ignored-path? "" path)
+                                   (not= (:file/name f) ".gitignore")))))
                       files)]
     (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
                                   %) files)]

+ 7 - 1
src/main/frontend/modules/outliner/core.cljs

@@ -92,7 +92,13 @@
      (when-let [block-id (get-in this [:data :block/uuid])]
        block-id)
      (when-let [db-id (get-in this [:data :db/id])]
-       (:block/uuid (db/pull db-id)))))
+       (let [uuid (:block/uuid (db/pull db-id))]
+         (if uuid
+           uuid
+           (let [new-id (db/new-block-id)]
+             (db/transact! {:db/id db-id
+                            :block/uuid new-id})
+             new-id))))))
 
   (-get-parent-id [this]
     (-> (get-in this [:data :block/parent])

+ 30 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -8,10 +8,12 @@
             [frontend.handler.search :as search-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.web.nfs :as nfs-handler]
+            [frontend.extensions.srs.handler :as srs]
             [frontend.modules.shortcut.before :as m]
             [frontend.state :as state]
             [frontend.util :refer [mac?]]))
 
+;; TODO: how to extend this for plugins usage? An atom?
 (def default-config
   {:shortcut.handler/date-picker
    {:date-picker/complete
@@ -53,6 +55,28 @@
      :binding "shift+enter"
      :fn      ui-handler/auto-complete-shift-complete}}
 
+   :shortcut.handler/cards
+   {:cards/toggle-answers
+    {:desc    "Cards: show/hide answers/clozes"
+     :binding "s"
+     :fn      srs/toggle-answers}
+    :cards/next-card
+    {:desc    "Cards: next card"
+     :binding "n"
+     :fn      srs/next-card}
+    :cards/forgotten
+    {:desc    "Cards: forgotten"
+     :binding "f"
+     :fn      srs/forgotten}
+    :cards/remembered
+    {:desc    "Cards: remembered"
+     :binding "r"
+     :fn      srs/remembered}
+    :cards/recall
+    {:desc    "Cards: take a while to recall"
+     :binding "t"
+     :fn      srs/recall}}
+
    :shortcut.handler/block-editing-only
    ^{:before m/enable-when-editing-mode!}
    {:editor/escape-editing
@@ -311,7 +335,7 @@
      :fn      state/toggle-theme!}
     :ui/toggle-contents
     {:desc    "Toggle Favorites in sidebar"
-     :binding "t c"
+     :binding "t f"
      :fn      ui-handler/toggle-contents!}
     :ui/toggle-wide-mode
     {:desc    "Toggle wide mode"
@@ -321,6 +345,10 @@
     {:desc    "Toggle open blocks (collapse or expand all blocks)"
      :binding "t o"
      :fn      editor-handler/toggle-open!}
+    :ui/toggle-cards
+    {:desc    "toggle cards"
+     :binding "t c"
+     :fn      ui-handler/toggle-cards!}
     ;; :ui/toggle-between-page-and-file route-handler/toggle-between-page-and-file!
     :git/commit
     {:desc    "Git commit message"
@@ -410,6 +438,7 @@
    [:ui/toggle-help
     :editor/toggle-open-blocks
     :ui/toggle-wide-mode
+    :ui/toggle-cards
     :ui/toggle-document-mode
     :ui/toggle-brackets
     :ui/toggle-theme

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

@@ -605,12 +605,13 @@
   (dom/add-class! block "selected noselect")
   (swap! state assoc
          :selection/mode true
-         :selection/blocks (conj (:selection/blocks @state) block)
+         :selection/blocks (conj (vec (:selection/blocks @state)) block)
          :selection/direction direction))
 
 (defn drop-last-selection-block!
   []
-  (let [last-block (peek (:selection/blocks @state))]
+  (def blocks (:selection/blocks @state))
+  (let [last-block (peek (vec (:selection/blocks @state)))]
     (swap! state assoc
            :selection/mode true
            :selection/blocks (vec (pop (:selection/blocks @state))))
@@ -1399,3 +1400,9 @@
 (defn get-favorites-name
   []
   (or (:name/favorites (get-config)) "Favorites"))
+
+(defn add-watch-state [key f]
+  (add-watch state key f))
+
+(defn remove-watch-state [key]
+  (remove-watch state key))

+ 9 - 5
src/main/frontend/ui.cljs

@@ -111,10 +111,12 @@
    opts))
 
 (defn button
-  [text & {:keys [background href class intent on-click]
+  [text & {:keys [background href class intent on-click small?]
+           :or {small? false}
            :as   option}]
   (let [klass (if-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700")
-        klass (if background (string/replace klass "indigo" background) klass)]
+        klass (if background (string/replace klass "indigo" background) klass)
+        klass (if small? (str klass ".px-2.py-1") klass)]
     (if href
       [:a.ui__button.is-link
        (merge
@@ -345,7 +347,7 @@
 (rum/defcs infinite-list <
   (mixins/event-mixin attach-listeners)
   "Render an infinite list."
-  [state list-element-id body {:keys [on-load has-more]}]
+  [state list-element-id body {:keys [on-load has-more on-top-reached]}]
   (rum/with-context [[t] i18n/*tongue-context*]
     (rum/fragment
      body
@@ -583,7 +585,8 @@
         (if (fn? header)
           (header @collapsed?)
           header)]]]
-     [:div {:class (if @collapsed? "hidden" "initial")}
+     [:div {:class (if @collapsed? "hidden" "initial")
+            :on-mouse-down (fn [e] (.stopPropagation e))}
       (if (fn? content)
         (if (not @collapsed?) (content) nil)
         content)]]))
@@ -661,7 +664,8 @@
                            (when-let [html (:html opts)]
                              (if (fn? html)
                                (html)
-                               html))
+                               [:div.pr-3.py-1
+                                html]))
                            [:div {:key "tippy"} ""])))
            child)))
 

+ 7 - 0
src/main/frontend/ui.css

@@ -111,6 +111,13 @@
   }
 }
 
+@media (min-width: 1024px) {
+    .panel-content .ls-card {
+        min-width: 32rem;
+        min-height: 24rem;
+    }
+}
+
 .ui__button {
   @apply inline-flex items-center px-3 py-2 border border-transparent
   text-sm leading-4 font-medium rounded-md text-white

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

@@ -283,6 +283,20 @@
              (parse-int x)
              (catch Exception _
                nil)))))
+#?(:cljs
+   (defn parse-float
+     [x]
+     (if (string? x)
+       (js/parseFloat x)
+       x)))
+
+#?(:cljs
+   (defn safe-parse-float
+     [x]
+     (let [result (parse-float x)]
+       (if (js/isNaN result)
+         nil
+         result))))
 
 #?(:cljs
    (defn debounce
@@ -381,6 +395,15 @@
                                   (- top 80)))
                          :behavior "smooth"}))))))
 
+#?(:cljs
+   (defn scroll-to-element-v2
+     [elem-id]
+     (when elem-id
+       (when-let [elem (gdom/getElement elem-id)]
+         (.scroll (app-scroll-container-node)
+                  #js {:top (element-top elem 0)
+                       :behavior "auto"})))))
+
 #?(:cljs
    (defn scroll-to
      ([pos]
@@ -393,11 +416,6 @@
                  #js {:top      pos
                       :behavior (if animate? "smooth" "auto")})))))
 
-#?(:cljs
-   (defn scroll-to-top
-     []
-     (scroll-to (app-scroll-container-node) 0 false)))
-
 #?(:cljs
    (defn scroll-top
      "Returns the scroll top position of the `node`. If `node` is not specified,
@@ -407,6 +425,18 @@
      ([node]
       (when node (.-scrollTop node)))))
 
+#?(:cljs
+   (defn scroll-to-top
+     []
+     (scroll-to (app-scroll-container-node) 0 false)))
+
+#?(:cljs
+   (defn scroll-to-bottom
+     [node]
+     (when-let [node ^js (or node (app-scroll-container-node))]
+       (let [bottom (.-scrollHeight node)]
+         (scroll-to node bottom false)))))
+
 (defn url-encode
   [string]
   #?(:cljs (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
@@ -742,7 +772,7 @@
        (js/document.body.removeChild el))))
 
 (def uuid-pattern "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}")
-(defonce exactly-uuid-pattern (re-pattern (str "^" uuid-pattern "$")))
+(defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
 (defn uuid-string?
   [s]
   (safe-re-find exactly-uuid-pattern s))
@@ -1302,3 +1332,20 @@
    (defn meta-key-name []
      (let [user-agent (.. js/navigator -userAgent)]
        (if mac? "Cmd" "Ctrl"))))
+
+;; TODO: share with electron
+(defn ignored-path?
+  [dir path]
+  (when (string? path)
+    (or
+     (some #(string/starts-with? path (str dir "/" %))
+           ["." ".recycle" "assets" "node_modules"])
+     (some #(string/includes? path (str "/" % "/"))
+           ["." ".recycle" "assets" "node_modules"])
+     ;; hidden directory or file
+     (re-find #"/\.[^.]+" path)
+     (re-find #"^\.[^.]+" path)
+     (let [path (string/lower-case path)]
+       (not
+        (some #(string/ends-with? path %)
+              [".md" ".markdown" ".org" ".edn" ".css"]))))))

+ 74 - 0
src/main/frontend/util/persist_var.cljs

@@ -0,0 +1,74 @@
+(ns frontend.util.persist-var
+  (:require [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.fs :as fs]
+            [frontend.util :as util]
+            [promesa.core :as p]))
+
+
+(defn- load-path [location]
+  (config/get-file-path (state/get-current-repo) (str config/app-name "/" location ".edn")))
+
+(defprotocol ILoad
+  (-load [this]))
+
+(defprotocol ISave
+  (-save [this]))
+
+(deftype PersistVar [*value location]
+  ILoad
+  (-load [_]
+    (state/add-watch-state (keyword (str "persist-var/" location))
+                           (fn [k r o n]
+                             (let [repo (state/get-current-repo)]
+                               (when (and
+                                      (not (get-in @*value [repo :loaded?]))
+                                      (get-in n [:nfs/user-granted? repo]))
+                                 (p/let [content (fs/read-file
+                                                  (config/get-repo-dir (state/get-current-repo))
+                                                  (load-path location))]
+                                   (when-let [content (and (some? content)
+                                                           (try (cljs.reader/read-string content)
+                                                                (catch js/Error e
+                                                                  (println (util/format "load persist-var failed: %s"  (load-path location)))
+                                                                  (js/console.dir e))))]
+                                     (swap! *value (fn [o]
+                                                     (-> o
+                                                         (assoc-in [repo :loaded?] true)
+                                                         (assoc-in [repo :value] content)))))))))))
+
+  ISave
+  (-save [_]
+    (let [path (load-path location)
+          repo (state/get-current-repo)
+          content (str (get-in @*value [repo :value]))
+          dir (config/get-repo-dir repo)]
+      (fs/write-file! repo dir path content nil)))
+
+  IDeref
+  (-deref [this]
+    (get-in @*value [(state/get-current-repo) :value]))
+
+  IReset
+  (-reset! [o new-value]
+    (swap! *value (fn [o] (assoc-in @*value [(state/get-current-repo) :value] new-value)))))
+
+(defn persist-var [init-value location]
+  "This var is stored at logseq/LOCATION.edn"
+  (let [var (->PersistVar (atom {(state/get-current-repo)
+                                 {:value init-value
+                                  :loaded? false}})
+                          location)]
+    (-load var)
+    var))
+
+(defn persist-save [v]
+  {:pre [(satisfies? ISave v)]}
+  (-save v))
+
+(comment
+  (do
+    (def bbb (persist-var 1 "aaa"))
+    (-save bbb)
+
+    ))

+ 14 - 6
src/main/frontend/util/property.cljs

@@ -12,16 +12,23 @@
 (defonce properties-end-pattern
   (re-pattern (util/format "%s[\t\r ]*\n|(%s\\s*$)" properties-end properties-end)))
 
-(def built-in-properties
+(def built-in-extended-properties (atom #{}))
+(defn register-built-in-properties
+  [props]
+  (reset! built-in-extended-properties (set/union @built-in-extended-properties props)))
+
+(defn built-in-properties
+  []
   (set/union
    #{:id :custom-id :background-color :heading :collapsed :created-at :updated-at :last-modified-at :created_at :last_modified_at :query-table :query-properties}
-   (set (map keyword config/markers))))
+   (set (map keyword config/markers))
+   @built-in-extended-properties))
 
 (defn properties-built-in?
   [properties]
   (and (seq properties)
        (let [ks (map (comp keyword string/lower-case name) (keys properties))]
-         (every? built-in-properties ks))))
+         (every? (built-in-properties) ks))))
 
 (defn contains-properties?
   [content]
@@ -102,7 +109,7 @@
 (defn with-built-in-properties
   [properties content format]
   (let [org? (= format :org)
-        properties (filter (fn [[k v]] (built-in-properties k)) properties)]
+        properties (filter (fn [[k v]] ((built-in-properties) k)) properties)]
     (if (seq properties)
       (let [[title & body] (string/split-lines content)
             properties-in-content? (and title (= (string/upper-case title) properties-start))
@@ -265,8 +272,9 @@
 
 (defn remove-built-in-properties
   [format content]
-  (let [content (reduce (fn [content key]
-                          (remove-property format key content)) content built-in-properties)]
+  (let [built-in-properties* (built-in-properties)
+        content (reduce (fn [content key]
+                          (remove-property format key content)) content built-in-properties*)]
     (if (= format :org)
       (string/replace-first content ":PROPERTIES:\n:END:" "")
       content)))

+ 14 - 0
src/main/frontend/utils.js

@@ -217,3 +217,17 @@ export const ios = function () {
   // iPad on iOS 13 detection
     || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
 }
+
+export const getClipText = function (cb, errorHandler) {
+  navigator.permissions.query({ name: "clipboard-read" }).then((result) => {
+    if (result.state == "granted" || result.state == "prompt") {
+      navigator.clipboard.readText()
+        .then(text => {
+          cb(text);
+        })
+        .catch(err => {
+          errorHandler(err)
+        });
+    }
+  })
+}

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

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

+ 6 - 0
tailwind.config.js

@@ -10,6 +10,12 @@ module.exports = {
   plugins: [require('@tailwindcss/ui')],
   darkMode: 'class',
   theme: {
+    extend: {
+      spacing: {
+        '128': '32rem',
+        '144': '36rem'
+      }
+    },
     colors: {
       transparent: 'transparent',
       current: 'currentColor',

+ 8 - 1
templates/config.edn

@@ -156,4 +156,11 @@
  ;;
  ;; With the default value of level 2, `b` will be collapsed.
  ;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed.
- :ref/default-open-blocks-level 2}
+ :ref/default-open-blocks-level 2
+
+ ;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5)
+ ;; :srs/learning-fraction 0.5
+
+ ;; the initial interval after the first successful review of a card (default 4)
+ ;; :srs/initial-interval 4
+ }

+ 4 - 4
yarn.lock

@@ -6165,10 +6165,10 @@ mkdirp@^0.5.0, mkdirp@^0.5.4, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
-mldoc@0.8.9:
-  version "0.8.9"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-0.8.9.tgz#801092f7fabfaa5a4173b05da3876d311e634dda"
-  integrity sha512-WIZMqQYPFAxQi1O8lkPcx1cxSCyzLSCr1x6ZvdTwhY3YnEUjywe2pD5poMg0meSkfEme7H6yX+/jKZLHPgNRUw==
[email protected].1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-0.9.1.tgz#dfcdcf52614a27ce83b9318c551246398e13f0e7"
+  integrity sha512-4BL8Fu6+izd9iJ3JhqEU57K9W8MHUD29V51eOXa/pTpmkXi1GFSy0c9nYLkd8KjAPkI6nFVmjl7A9rcfbGe+/g==
   dependencies:
     yargs "^12.0.2"
 

Some files were not shown because too many files changed in this diff