Sfoglia il codice sorgente

Merge branch 'util-to-cljc' of git://github.com/adamschmideg/logseq into adamschmideg-util-to-cljc

Tienson Qin 5 anni fa
parent
commit
60406cbb10
4 ha cambiato i file con 1112 aggiunte e 1036 eliminazioni
  1. 0 0
      src/main/frontend/regex.cljc
  2. 0 13
      src/main/frontend/util.clj
  3. 1112 0
      src/main/frontend/util.cljc
  4. 0 1023
      src/main/frontend/util.cljs

+ 0 - 0
src/main/frontend/regex.cljs → src/main/frontend/regex.cljc


+ 0 - 13
src/main/frontend/util.clj

@@ -1,13 +0,0 @@
-(ns frontend.util)
-
-;; Copied from https://github.com/tonsky/datascript-todo
-(defmacro profile [k & body]
-  `(if goog.DEBUG
-     (let [k# ~k]
-       (.time js/console k#)
-       (let [res# (do ~@body)]
-         (.timeEnd js/console k#)
-         res#))
-     (do ~@body)))
-
-;; TODO: profile and profileEnd

+ 1112 - 0
src/main/frontend/util.cljc

@@ -0,0 +1,1112 @@
+(ns frontend.util
+  (:require
+      #?(:cljs [cljs-bean.core :as bean])
+      #?(:cljs [cljs-time.coerce :as tc])
+      #?(:cljs [cljs-time.core :as t])
+      #?(:cljs [cljs-time.format :as format])
+      #?(:cljs [dommy.core :as d])
+      #?(:cljs ["/frontend/caret_pos" :as caret-pos])
+      #?(:cljs ["/frontend/selection" :as selection])
+      #?(:cljs ["/frontend/utils" :as utils])
+      #?(:cljs [goog.dom :as gdom])
+      #?(:cljs [goog.object :as gobj])
+      #?(:cljs [goog.string :as gstring])
+      #?(:cljs [goog.string.format])
+      #?(:cljs [goog.userAgent])
+      [clojure.string :as string]
+      [clojure.pprint :refer [pprint]]
+      [clojure.walk :as walk]
+      [frontend.regex :as regex]
+      [promesa.core :as p]))
+
+#?(:cljs (goog-define NODETEST false)
+   :clj (def NODETEST false))
+(defonce node-test? NODETEST)
+
+#?(:cljs
+    (extend-protocol IPrintWithWriter
+      js/Symbol
+      (-pr-writer [sym writer _]
+        (-write writer (str "\"" (.toString sym) "\"")))))
+
+;; doms
+#?(:cljs (defn html-node []  js/document.documentElement))
+
+#?(:cljs
+    (defn ios?
+      []
+      (not (nil? (re-find #"iPad|iPhone|iPod" js/navigator.userAgent)))))
+
+#?(:cljs
+    (defn safari?
+      []
+      (let [ua (string/lower-case js/navigator.userAgent)]
+        (and (string/includes? ua "webkit")
+             (not (string/includes? ua "chrome"))))))
+
+#?(:cljs
+    (defn mobile?
+      []
+      (when-not node-test?
+        (re-find #"Mobi" js/navigator.userAgent))))
+
+(defn format
+  [fmt & args]
+  #?(:cljs (apply gstring/format fmt args)
+     :clj (apply clojure.core/format fmt args)))
+
+#?(:cljs
+    (defn evalue
+      [event]
+      (gobj/getValueByKeys event "target" "value")))
+
+#?(:cljs
+    (defn set-change-value
+      "compatible change event for React"
+      [node value]
+      (utils/triggerInputChange node value)))
+
+#?(:cljs
+    (defn p-handle
+      ([p ok-handler]
+       (p-handle p ok-handler (fn [error]
+                                (js/console.error error))))
+      ([p ok-handler error-handler]
+       (-> p
+           (p/then (fn [result]
+                     (ok-handler result)))
+           (p/catch (fn [error]
+                      (error-handler error)))))))
+
+#?(:cljs
+    (defn get-width
+      []
+      (gobj/get js/window "innerWidth")))
+
+(defn indexed
+  [coll]
+  (map-indexed vector coll))
+
+(defn find-first
+  [pred coll]
+  (first (filter pred coll)))
+
+(defn dissoc-in
+  "Dissociates an entry from a nested associative structure returning a new
+  nested structure. keys is a sequence of keys. Any empty maps that result
+  will not be present in the new structure."
+  [m [k & ks :as keys]]
+  (if ks
+    (if-let [nextmap (get m k)]
+      (let [newmap (dissoc-in nextmap ks)]
+        (if (seq newmap)
+          (assoc m k newmap)
+          (dissoc m k)))
+      m)
+    (dissoc m k)))
+
+;; (defn format
+;;   [fmt & args]
+;;   (apply gstring/format fmt args))
+
+(defn json->clj
+  [json-string]
+  #?(:cljs
+      (-> json-string
+          (js/JSON.parse)
+          (js->clj :keywordize-keys true))))
+
+(defn remove-nils
+  "remove pairs of key-value that has nil value from a (possibly nested) map. also transform map to nil if all of its value are nil"
+  [nm]
+  (walk/postwalk
+   (fn [el]
+     (if (map? el)
+       (not-empty (into {} (remove (comp nil? second)) el))
+       el))
+   nm))
+
+(defn remove-nils-or-empty
+  [nm]
+  (walk/postwalk
+   (fn [el]
+     (if (map? el)
+       (not-empty (into {} (remove (comp #(or
+                                           (nil? %)
+                                           (and (coll? %)
+                                                (empty? %))) second)) el))
+       el))
+   nm))
+
+(defn index-by
+  [col k]
+  (->> (map (fn [entry] [(get entry k) entry])
+            col)
+       (into {})))
+
+;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
+(defn hiccup->class
+  [class]
+  (some->> (string/split class #"\.")
+           (string/join " ")
+           (string/trim)))
+
+#?(:cljs
+   (defn fetch-raw
+     ([url on-ok on-failed]
+      (fetch-raw url {} on-ok on-failed))
+     ([url opts on-ok on-failed]
+      (-> (js/fetch url (bean/->js opts))
+          (.then (fn [resp]
+                   (if (>= (.-status resp) 400)
+                     (on-failed resp)
+                     (if (.-ok resp)
+                       (-> (.text resp)
+                           (.then bean/->clj)
+                           (.then #(on-ok %)))
+                       (on-failed resp)))))))))
+
+#?(:cljs
+   (defn fetch
+     ([url on-ok on-failed]
+      (fetch url {} on-ok on-failed))
+     ([url opts on-ok on-failed]
+      (-> (js/fetch url (bean/->js opts))
+          (.then (fn [resp]
+                   (if (>= (.-status resp) 400)
+                     (on-failed resp)
+                     (if (.-ok resp)
+                       (-> (.json resp)
+                           (.then bean/->clj)
+                           (.then #(on-ok %)))
+                       (on-failed resp)))))))))
+
+#?(:cljs
+   (defn upload
+     [url file on-ok on-failed on-progress]
+     (let [xhr (js/XMLHttpRequest.)]
+       (.open xhr "put" url)
+       (gobj/set xhr "onload" on-ok)
+       (gobj/set xhr "onerror" on-failed)
+       (when (and (gobj/get xhr "upload")
+                  on-progress)
+         (gobj/set (gobj/get xhr "upload")
+                   "onprogress"
+                   on-progress))
+       (.send xhr file))))
+
+(defn post
+  [url body on-ok on-failed]
+  #?(:cljs
+      (fetch url {:method "post"
+                  :headers {:Content-Type "application/json"}
+                  :body (js/JSON.stringify (clj->js body))}
+             on-ok
+             on-failed)))
+
+(defn patch
+  [url body on-ok on-failed]
+  #?(:cljs
+      (fetch url {:method "patch"
+                  :headers {:Content-Type "application/json"}
+                  :body (js/JSON.stringify (clj->js body))}
+             on-ok
+             on-failed)))
+
+(defn delete
+  [url on-ok on-failed]
+  #?(:cljs
+      (fetch url {:method "delete"
+                  :headers {:Content-Type "application/json"}}
+             on-ok
+             on-failed)))
+
+(defn zero-pad
+  [n]
+  (if (< n 10)
+    (str "0" n)
+    (str n)))
+
+(defn parse-int
+  [x]
+  #?(:cljs (if (string? x)
+             (js/parseInt x)
+             x)
+     :clj (if (string? x)
+            (Integer/parseInt x)
+            x)))
+
+
+(defn safe-parse-int
+  [x]
+  #?(:cljs (let [result (parse-int x)]
+             (if (js/isNaN result)
+               nil
+               result))
+     :clj ((try
+             (parse-int x)
+             (catch Exception _
+               nil)))))
+
+#?(:cljs
+    (defn debounce
+      "Returns a function that will call f only after threshold has passed without new calls
+      to the function. Calls prep-fn on the args in a sync way, which can be used for things like
+      calling .persist on the event object to be able to access the event attributes in f"
+      ([threshold f] (debounce threshold f (constantly nil)))
+      ([threshold f prep-fn]
+       (let [t (atom nil)]
+         (fn [& args]
+           (when @t (js/clearTimeout @t))
+           (apply prep-fn args)
+           (reset! t (js/setTimeout #(do
+                                       (reset! t nil)
+                                       (apply f args))
+                                    threshold)))))))
+
+;; Caret
+#?(:cljs
+    (defn caret-range [node]
+      (let [doc (or (gobj/get node "ownerDocument")
+                    (gobj/get node "document"))
+            win (or (gobj/get doc "defaultView")
+                    (gobj/get doc "parentWindow"))
+            selection (.getSelection win)]
+        (if selection
+          (let [range-count (gobj/get selection "rangeCount")]
+            (when (> range-count 0)
+              (let [range (-> (.getSelection win)
+                              (.getRangeAt 0))
+                    pre-caret-range (.cloneRange range)]
+                (.selectNodeContents pre-caret-range node)
+                (.setEnd pre-caret-range
+                         (gobj/get range "endContainer")
+                         (gobj/get range "endOffset"))
+                (.toString pre-caret-range))))
+          (when-let [selection (gobj/get doc "selection")]
+            (when (not= "Control" (gobj/get selection "type"))
+              (let [text-range (.createRange selection)
+                    pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
+                (.moveToElementText pre-caret-text-range node)
+                (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
+                (gobj/get pre-caret-text-range "text"))))))))
+
+#?(:cljs
+    (defn set-caret-pos!
+      [input pos]
+      (.setSelectionRange input pos pos)))
+
+#?(:cljs
+    (defn get-caret-pos
+      [input]
+      (try
+        (let [pos ((gobj/get caret-pos "position") input)]
+          (set! pos -rect (.. input (getBoundingClientRect) (toJSON)))
+          (bean/->clj pos))
+        (catch js/Error e
+          (js/console.error e)))))
+
+(defn minimize-html
+  [s]
+  (->> s
+       (string/split-lines)
+       (map string/trim)
+       (string/join "")))
+
+#?(:cljs
+    (defn stop [e]
+      (doto e (.preventDefault) (.stopPropagation))))
+
+#?(:cljs
+    (defn get-fragment
+      []
+      (when-let [hash js/window.location.hash]
+        (when (> (count hash) 2)
+          (-> (subs hash 1)
+              (string/split #"\?")
+              (first))))))
+
+;; (defn scroll-into-view
+;;   [element]
+;;   (let [scroll-top (gobj/get element "offsetTop")
+;;         scroll-top (if (zero? scroll-top)
+;;                      (-> (gobj/get element "parentElement")
+;;                          (gobj/get "offsetTop"))
+;;                      scroll-top)]
+;;     (prn {:scroll-top scroll-top})
+;;     (when-let [main (gdom/getElement "main-content")]
+;;       (prn {:main main})
+;;       (.scroll main #js {:top scroll-top
+;;                          ;; :behavior "smooth"
+;;                          }))))
+
+;; (defn scroll-to-element
+;;   [fragment]
+;;   (when fragment
+;;     (prn {:fragment fragment})
+;;     (when-not (string/blank? fragment)
+;;       (when-let [element (gdom/getElement fragment)]
+;;         (scroll-into-view element)))))
+
+(def speed 500)
+(def moving-frequency 15)
+
+#?(:cljs
+    (defn cur-doc-top []
+      (.. js/document -documentElement -scrollTop)))
+
+#?(:cljs
+    (defn element-top [elem top]
+      (when elem
+        (if (.-offsetParent elem)
+          (let [client-top (or (.-clientTop elem) 0)
+                offset-top (.-offsetTop elem)]
+            (+ top client-top offset-top (element-top (.-offsetParent elem) top)))
+          top))))
+
+#?(:cljs
+    (defn scroll-to-element
+      [elem-id]
+      (when-not (re-find #"^/\d+$" elem-id)
+        (when elem-id
+          (when-let [elem (gdom/getElement elem-id)]
+            (.scroll (html-node)
+                     #js {:top (let [top (element-top elem 0)]
+                                 (if (> top 68)
+                                   (- top 68)
+                                   top))
+                          :behavior "smooth"}))))))
+
+#?(:cljs
+    (defn scroll-to
+      ([pos]
+       (scroll-to (html-node) pos))
+      ([node pos]
+       (.scroll node
+                #js {:top      pos
+                     :behavior "smooth"}))))
+
+#?(:cljs
+    (defn scroll-to-top
+      []
+      (scroll-to 0)))
+
+(defn url-encode
+  [string]
+  #?(:cljs (some-> string str (js/encodeURIComponent) (.replace "+" "%20"))))
+
+(defn url-decode
+  [string]
+  #?(:cljs (some-> string str (js/decodeURIComponent))))
+
+#?(:cljs
+    (defn link?
+      [node]
+      (contains?
+       #{"A" "BUTTON"}
+       (gobj/get node "tagName"))))
+
+#?(:cljs
+    (defn sup?
+      [node]
+      (contains?
+       #{"SUP"}
+       (gobj/get node "tagName"))))
+
+#?(:cljs
+    (defn input?
+      [node]
+      (when node
+        (contains?
+         #{"INPUT" "TEXTAREA"}
+         (gobj/get node "tagName")))))
+
+#?(:cljs
+    (defn select?
+      [node]
+      (when node
+        (= "SELECT" (gobj/get node "tagName")))))
+
+#?(:cljs
+    (defn details-or-summary?
+      [node]
+      (when node
+        (contains?
+         #{"DETAILS" "SUMMARY"}
+         (gobj/get node "tagName")))))
+
+;; Debug
+(defn starts-with?
+  [s substr]
+  (string/starts-with? s substr))
+
+(defn journal?
+  [path]
+  (starts-with? path "journals/"))
+
+(defn drop-first-line
+  [s]
+  (let [lines (string/split-lines s)
+        others (some->> (next lines)
+                        (string/join "\n"))]
+    [(first lines)]))
+
+(defn distinct-by
+  [f col]
+  (reduce
+   (fn [acc x]
+     (if (some #(= (f x) (f %)) acc)
+       acc
+       (vec (conj acc x))))
+   []
+   col))
+
+(defn distinct-by-last-wins
+  [f col]
+  (reduce
+   (fn [acc x]
+     (if (some #(= (f x) (f %)) acc)
+       (mapv
+        (fn [v]
+          (if (= (f x) (f v))
+            x
+            v))
+        acc)
+       (vec (conj acc x))))
+   []
+   col))
+
+(defn get-git-owner-and-repo
+  [repo-url]
+  (take-last 2 (string/split repo-url #"/")))
+
+#?(:cljs
+    (defn get-textarea-height
+      [input]
+      (some-> input
+              (d/style)
+              (gobj/get "height")
+              (string/split #"\.")
+              first
+              (parse-int))))
+
+#?(:cljs
+    (defn get-textarea-line-height
+      [input]
+      (try
+        (some-> input
+                (d/style)
+                (gobj/get "lineHeight")
+                ;; TODO: is this cross-platform?
+                (string/replace "px" "")
+                (parse-int))
+        (catch js/Error _e
+          24))))
+
+#?(:cljs
+    (defn textarea-cursor-first-row?
+      [input line-height]
+      (<= (:top (get-caret-pos input)) line-height)))
+
+#?(:cljs
+    (defn textarea-cursor-end-row?
+      [input line-height]
+      (>= (+ (:top (get-caret-pos input)) line-height)
+          (get-textarea-height input))))
+
+(defn safe-split-first [pattern s]
+  (if-let [first-index (string/index-of s pattern)]
+    [(subs s 0 first-index)
+     (subs s (+ first-index (count pattern)) (count s))]
+    [s ""]))
+
+(defn split-first [pattern s]
+  (when-let [first-index (string/index-of s pattern)]
+    [(subs s 0 first-index)
+     (subs s (+ first-index (count pattern)) (count s))]))
+
+(defn split-last [pattern s]
+  (when-let [last-index (string/last-index-of s pattern)]
+    [(subs s 0 last-index)
+     (subs s (+ last-index (count pattern)) (count s))]))
+
+(defn trim-safe
+  [s]
+  (when s
+    (string/trim s)))
+
+(defn trimr-without-newlines
+  [s]
+  (.replace s #"[ \t\r]+$" ""))
+
+(defn trim-only-newlines
+  [s]
+  (-> s
+      (.replace #"[\n]+$" "")
+      (.replace #"^[\n]+" "")))
+
+(defn triml-without-newlines
+  [s]
+  (.replace s #"^[ \t\r]+" ""))
+
+(defn concat-without-spaces
+  [left right]
+  (when (and (string? left)
+             (string? right))
+    (let [left (trimr-without-newlines left)
+          not-space? (or
+                      (string/blank? left)
+                      (= "\n" (last left)))]
+      (str left
+           (when-not not-space? " ")
+           (triml-without-newlines right)))))
+
+(defn join-newline
+  [& col]
+  #?(:cljs
+      (let [col (remove nil? col)]
+        (reduce (fn [acc s]
+                  (if (or (= acc "") (= "\n" (last acc)))
+                    (str acc s)
+                    (str acc "\n"
+                         (.replace s #"^[\n]+" "")))) "" col))))
+
+;; Add documentation
+(defn replace-first [pattern s new-value]
+  (when-let [first-index (string/index-of s pattern)]
+    (str new-value (subs s (+ first-index (count pattern))))))
+
+(defn replace-last [pattern s new-value]
+  (when-let [last-index (string/last-index-of s pattern)]
+    (concat-without-spaces
+     (subs s 0 last-index)
+     new-value)))
+
+;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
+#?(:cljs
+    (defn re-pos [re s]
+      (let [re (js/RegExp. (.-source re) "g")]
+        (loop [res []]
+          (if-let [m (.exec re s)]
+            (recur (conj res [(.-index m) (first m)]))
+            res)))))
+
+#?(:cljs
+    (defn cursor-move-back [input n]
+      (let [{:keys [pos]} (get-caret-pos input)]
+        (set! (.-selectionStart input) (- pos n))
+        (set! (.-selectionEnd input) (- pos n)))))
+
+#?(:cljs
+    (defn cursor-move-forward [input n]
+      (let [{:keys [pos]} (get-caret-pos input)]
+        (set! (.-selectionStart input) (+ pos n))
+        (set! (.-selectionEnd input) (+ pos n)))))
+
+#?(:cljs
+    (defn move-cursor-to [input n]
+      (set! (.-selectionStart input) n)
+      (set! (.-selectionEnd input) n)))
+
+#?(:cljs
+    (defn move-cursor-to-end
+      [input]
+      (let [pos (count (gobj/get input "value"))]
+        (move-cursor-to input pos))))
+
+;; copied from re_com
+#?(:cljs
+    (defn deref-or-value
+      "Takes a value or an atom
+      If it's a value, returns it
+      If it's a Reagent object that supports IDeref, returns the value inside it by derefing
+      "
+      [val-or-atom]
+      (if (satisfies? IDeref val-or-atom)
+        @val-or-atom
+        val-or-atom)))
+
+;; copied from re_com
+#?(:cljs
+    (defn now->utc
+      "Return a goog.date.UtcDateTime based on local date/time."
+      []
+      (let [local-date-time (js/goog.date.DateTime.)]
+        (js/goog.date.UtcDateTime.
+         (.getYear local-date-time)
+         (.getMonth local-date-time)
+         (.getDate local-date-time)
+         0 0 0 0))))
+
+(defn safe-subvec [xs start end]
+  (if (or (neg? start)
+          (> end (count xs)))
+    []
+    (subvec xs start end)))
+
+(defn safe-subs
+  ([s start]
+   (let [c (count s)]
+     (safe-subs s start c)))
+  ([s start end]
+   (let [c (count s)]
+     (subs s (min c start) (min c end)))))
+
+#?(:cljs
+    (defn get-nodes-between-two-nodes
+      [id1 id2 class]
+      (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
+        (let [id #(gobj/get % "id")
+              node-1 (gdom/getElement id1)
+              node-2 (gdom/getElement id2)
+              idx-1 (.indexOf nodes node-1)
+              idx-2 (.indexOf nodes node-2)
+              start (min idx-1 idx-2)
+              end (inc (max idx-1 idx-2))]
+          (safe-subvec (vec nodes) start end)))))
+
+#?(:cljs
+    (defn rec-get-block-node
+      [node]
+      (if (and node (d/has-class? node "ls-block"))
+        node
+        (and node
+             (rec-get-block-node (gobj/get node "parentNode"))))))
+
+#?(:cljs
+    (defn rec-get-blocks-container
+      [node]
+      (if (and node (d/has-class? node "blocks-container"))
+        node
+        (and node
+             (rec-get-blocks-container (gobj/get node "parentNode"))))))
+
+#?(:cljs
+    (defn rec-get-blocks-content-section
+      [node]
+      (if (and node (d/has-class? node "content"))
+        node
+        (and node
+             (rec-get-blocks-content-section (gobj/get node "parentNode"))))))
+
+;; Take the idea from https://stackoverflow.com/questions/4220478/get-all-dom-block-elements-for-selected-texts.
+;; FIXME: Note that it might not works for IE.
+#?(:cljs
+    (defn get-selected-nodes
+      [class-name]
+      (try
+        (when (gobj/get js/window "getSelection")
+          (let [selection (js/window.getSelection)
+                range (.getRangeAt selection 0)
+                container (-> (gobj/get range "commonAncestorContainer")
+                              (rec-get-blocks-container))
+                start-node (gobj/get range "startContainer")
+                container-nodes (array-seq (selection/getSelectedNodes container start-node))]
+            (map
+             (fn [node]
+               (if (or (= 3 (gobj/get node "nodeType"))
+                       (not (d/has-class? node class-name))) ;textnode
+                 (rec-get-block-node node)
+                 node))
+             container-nodes)))
+        (catch js/Error _e
+          nil))))
+
+#?(:cljs
+    (defn get-input-pos
+      [input]
+      (and input (.-selectionStart input))))
+
+#?(:cljs
+    (defn get-selected-text
+      []
+      (utils/getSelectionText)))
+
+#?(:cljs (def clear-selection! selection/clearSelection))
+
+#?(:cljs
+    (defn copy-to-clipboard! [s]
+      (let [el (js/document.createElement "textarea")]
+        (set! (.-value el) s)
+        (.setAttribute el "readonly" "")
+        (set! (-> el .-style .-position) "absolute")
+        (set! (-> el .-style .-left) "-9999px")
+        (js/document.body.appendChild el)
+        (.select el)
+        (js/document.execCommand "copy")
+        (js/document.body.removeChild el))))
+
+(defn take-at-most
+  [s n]
+  (if (<= (count s) n)
+    s
+    (subs s 0 n)))
+(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 "$")))
+(defn uuid-string?
+  [s]
+  (re-find exactly-uuid-pattern s))
+
+(defn extract-uuid
+  [s]
+  (re-find (re-pattern uuid-pattern) s))
+
+(defn drop-nth [n coll]
+  (keep-indexed #(if (not= %1 n) %2) coll))
+
+(defn capitalize-all [s]
+  (some->> (string/split s #" ")
+           (map string/capitalize)
+           (string/join " ")))
+
+(defn file-page?
+  [page-name]
+  (when page-name (re-find #"\." page-name)))
+
+;; Remove rum *reactions* assert
+#?(:cljs
+    (defn react
+      "Works in conjunction with [[reactive]] mixin. Use this function instead of `deref` inside render, and your component will subscribe to changes happening to the derefed atom."
+      [ref]
+      (when rum.core/*reactions*
+        (vswap! rum.core/*reactions* conj ref))
+      (and ref @ref)))
+
+(defn time-ms
+  []
+  #?(:cljs (tc/to-long (cljs-time.core/now))))
+
+(defn get-repo-dir
+  [repo-url]
+  (str "/"
+       (->> (take-last 2 (string/split repo-url #"/"))
+            (string/join "_"))))
+
+(defn d
+  [k f]
+  (let [result (atom nil)]
+    (println (str "Debug " k))
+    (time (reset! result (doall (f))))
+    @result))
+
+(defn concat-without-nil
+  [& cols]
+  (->> (apply concat cols)
+       (remove nil?)))
+
+#?(:cljs
+    (defn set-title!
+      [title]
+      (set! (.-title js/document) title)))
+
+#?(:cljs
+    (defn get-prev-block
+      [block]
+      (when-let [blocks (d/by-class "ls-block")]
+        (when-let [index (.indexOf blocks block)]
+          (when (> index 0)
+            (nth blocks (dec index)))))))
+
+#?(:cljs
+    (defn get-next-block
+      [block]
+      (when-let [blocks (d/by-class "ls-block")]
+        (when-let [index (.indexOf blocks block)]
+          (when (> (count blocks) (inc index))
+            (nth blocks (inc index)))))))
+
+#?(:cljs
+    (defn get-prev-block-with-same-level
+      [block]
+      (let [id (gobj/get block "id")
+            prefix (re-find #"ls-block-[\d]+" id)]
+        (when-let [blocks (d/by-class "ls-block")]
+          (when-let [index (.indexOf blocks block)]
+            (let [level (d/attr block "level")]
+              (when (> index 0)
+                (loop [idx (dec index)]
+                  (if (>= idx 0)
+                    (let [block (nth blocks idx)
+                          prefix-match? (starts-with? (gobj/get block "id") prefix)]
+                      (if (and prefix-match?
+                               (= level (d/attr block "level")))
+                        block
+                        (recur (dec idx))))
+                    nil)))))))))
+
+#?(:cljs
+    (defn get-next-block-with-same-level
+      [block]
+      (when-let [blocks (d/by-class "ls-block")]
+        (when-let [index (.indexOf blocks block)]
+          (let [level (d/attr block "level")]
+            (when (> (count blocks) (inc index))
+              (loop [idx (inc index)]
+                (if (< idx (count blocks))
+                  (let [block (nth blocks idx)]
+                    (if (= level (d/attr block "level"))
+                      block
+                      (recur (inc idx))))
+                  nil))))))))
+
+#?(:cljs
+    (defn get-block-idx-inside-container
+      [block-element]
+      (when block-element
+        (when-let [section (some-> (rec-get-blocks-content-section block-element)
+                              (d/parent))]
+          (let [blocks (d/by-class section "ls-block")
+                idx (when (seq blocks) (.indexOf (array-seq blocks) block-element))]
+            (when (and idx section)
+             {:idx idx
+              :container (gdom/getElement section "id")}))))))
+
+(defn nth-safe [c i]
+  (if (or (< i 0) (>= i (count c)))
+    nil
+    (nth c i)))
+
+(defn sort-by-value
+  [order m]
+  (into (sorted-map-by
+         (fn [k1 k2]
+           (let [v1 (get m k1)
+                 v2 (get m k2)]
+             (if (= order :desc)
+               (compare [v2 k2] [v1 k1])
+               (compare [v1 k1] [v2 k2])))))
+        m))
+
+(defn rand-str
+  [n]
+  #?(:cljs (-> (.toString (js/Math.random) 36)
+               (.substr 2 n))
+     :clj (->> (repeatedly #(Integer/toString (rand 36) 36))
+               (take n)
+               (apply str))))
+
+(defn unique-id
+  []
+  (str (rand-str 6) (rand-str 3)))
+
+(defn tag-valid?
+  [tag-name]
+  (when tag-name
+    (and
+     (not (re-find #"#" tag-name))
+     (re-find regex/valid-tag-pattern tag-name))))
+
+(defn encode-str
+  [s]
+  (if (tag-valid? s)
+    s
+    (url-encode s)))
+
+#?(:cljs
+    (defn- get-clipboard-as-html
+      [event]
+      (if-let [c (gobj/get event "clipboardData")]
+        [(.getData c "text/html") (.getData c "text")]
+        (if-let [c (gobj/getValueByKeys event "originalEvent" "clipboardData")]
+          [(.getData c "text/html") (.getData c "text")]
+          (if-let [c (gobj/get js/window "clipboardData")]
+            [(.getData c "Text") (.getData c "Text")])))))
+
+(defn marker?
+  [s]
+  (contains?
+   #{"NOW" "LATER" "TODO" "DOING"
+     "DONE" "WAIT" "WAITING" "CANCELED" "CANCELLED" "STARTED" "IN-PROGRESS"}
+   (string/upper-case s)))
+
+(defn pp-str [x]
+  (with-out-str (pprint x)))
+
+(defn hiccup-keywordize
+  [hiccup]
+  (walk/postwalk
+   (fn [f]
+     (if (and (vector? f) (string? (first f)))
+       (update f 0 keyword)
+       f))
+   hiccup))
+
+#?(:cljs
+    (defn chrome?
+      []
+      (let [user-agent js/navigator.userAgent
+            vendor js/navigator.vendor]
+        (and (re-find #"Chrome" user-agent)
+             (re-find #"Google Inc" user-agent)))))
+
+#?(:cljs
+    (defn indexeddb-check?
+      [error-handler]
+      (let [test-db "logseq-test-db-foo-bar-baz"
+            db (and js/window.indexedDB
+                    (js/window.indexedDB.open test-db))]
+        (when (and db (not (chrome?)))
+          (gobj/set db "onerror" error-handler)
+          (gobj/set db "onsuccess"
+                    (fn []
+                      (js/window.indexedDB.deleteDatabase test-db)))))))
+
+(defonce mac? #?(:cljs goog.userAgent/MAC
+                 :clj nil))
+
+(defn ->system-modifier
+  [keyboard-shortcut]
+  (if mac?
+    (-> keyboard-shortcut
+        (string/replace "ctrl" "meta")
+        (string/replace "alt" "meta"))
+    keyboard-shortcut))
+
+(defn default-content-with-title
+  ([text-format title]
+   (default-content-with-title text-format title true))
+  ([text-format title new-block?]
+   (let [contents? (= (string/lower-case title) "contents")
+         properties (case (name text-format)
+                      "org"
+                      (format "#+TITLE: %s" title)
+                      "markdown"
+                      (format "---\ntitle: %s\n---" title)
+                      "")
+         new-block (case (name text-format)
+                     "org"
+                     "** "
+                     "markdown"
+                     "## "
+                     "")]
+     (if contents?
+       new-block
+       (str properties "\n\n" (if new-block? new-block))))))
+
+#?(:cljs
+    (defn get-first-block-by-id
+      [block-id]
+      (when block-id
+        (let [block-id (str block-id)]
+          (when (uuid-string? block-id)
+            (first (array-seq (js/document.getElementsByClassName block-id))))))))
+
+(defn page-name-sanity
+  [page-name]
+  (-> page-name
+      (string/replace #"\s+" "_")
+      ;; Windows reserved path characters
+      (string/replace #"[\\/:\\*\\?\"<>|]+" "_")))
+
+(defn lowercase-first
+  [s]
+  (when s
+    (str (string/lower-case (.charAt s 0))
+         (subs s 1))))
+
+#?(:cljs
+    (defn add-style!
+      [style]
+      (when (some? style)
+        (let [parent-node (d/sel1 :head)
+              id "logseq-custom-theme-id"
+              old-link-element (d/sel1 id)
+              style (if (string/starts-with? style "http")
+                      style
+                      (str "data:text/css;charset=utf-8," (js/encodeURIComponent style)))]
+          (when old-link-element
+            (d/remove! old-link-element))
+          (let [link (->
+                      (d/create-element :link)
+                      (d/set-attr! :id id)
+                      (d/set-attr! :rel "stylesheet")
+                      (d/set-attr! :type "text/css")
+                      (d/set-attr! :href style)
+                      (d/set-attr! :media "all"))]
+            (d/append! parent-node link))))))
+
+(defn ->platform-shortcut
+  [keyboard-shortcut]
+  (if mac?
+    (-> keyboard-shortcut
+        (string/replace "Ctrl" "Cmd")
+        (string/replace "Alt" "Opt"))
+    keyboard-shortcut))
+
+(defn remove-common-preceding
+  [col1 col2]
+  (if (and (= (first col1) (first col2))
+           (seq col1))
+    (recur (rest col1) (rest col2))
+    [col1 col2]))
+
+;; fs
+(defn get-file-ext
+  [file]
+  (last (string/split file #"\.")))
+
+(defn get-dir-and-basename
+  [path]
+  (let [parts (string/split path "/")
+        basename (last parts)
+        dir (->> (butlast parts)
+                 (string/join "/"))]
+    [dir basename]))
+
+(defn get-relative-path
+  [current-file-path another-file-path]
+  (let [directories-f #(butlast (string/split % "/"))
+        parts-1 (directories-f current-file-path)
+        parts-2 (directories-f another-file-path)
+        [parts-1 parts-2] (remove-common-preceding parts-1 parts-2)
+        another-file-name (last (string/split another-file-path "/"))]
+    (->> (concat
+          (if (seq parts-1)
+            (repeat (count parts-1) "..")
+            ["."])
+          parts-2
+          [another-file-name])
+         (string/join "/"))))
+
+;; Copied from https://github.com/tonsky/datascript-todo
+(defmacro profile [k & body]
+  #?(:clj
+      `(if goog.DEBUG
+         (let [k# ~k]
+           (.time js/console k#)
+           (let [res# (do ~@body)]
+             (.timeEnd js/console k#)
+             res#))
+         (do ~@body))))
+
+;; TODO: profile and profileEnd
+
+;; Copy from hiccup
+(defn escape-html
+  "Change special characters into HTML character entities."
+  [text]
+  (-> text
+      (string/replace "&"  "&amp;")
+      (string/replace "<"  "&lt;")
+      (string/replace ">"  "&gt;")
+      (string/replace "\"" "&quot;")
+      (string/replace "'" "&apos;")))
+
+(defn unescape-html
+  [text]
+  (-> text
+      (string/replace "&amp;" "&")
+      (string/replace "&lt;" "<")
+      (string/replace "&gt;" ">")
+      (string/replace "&quot;" "\"")
+      (string/replace "&apos;" "'")))
+
+(comment
+  (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
+     "../pages/grant_ideas.org")
+
+  (= (get-relative-path "journals/2020_11_18.org" "journals/2020_11_19.org")
+     "./2020_11_19.org")
+
+  (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
+     "../e/f.org"))

+ 0 - 1023
src/main/frontend/util.cljs

@@ -1,1023 +0,0 @@
-(ns frontend.util
-  (:require [goog.object :as gobj]
-            [goog.dom :as gdom]
-            [promesa.core :as p]
-            [clojure.walk :as walk]
-            [clojure.string :as string]
-            [cljs-bean.core :as bean]
-            [clojure.string :as string]
-            ["/frontend/caret_pos" :as caret-pos]
-            ["/frontend/selection" :as selection]
-            ["/frontend/utils" :as utils]
-            [goog.string :as gstring]
-            [goog.string.format]
-            [dommy.core :as d]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
-            [cljs-time.format :as format]
-            [frontend.regex :as regex]
-            [clojure.pprint :refer [pprint]]
-            [goog.userAgent]
-            [goog.string :as gstring]))
-
-(goog-define NODETEST false)
-(defonce node-test? NODETEST)
-
-(extend-protocol IPrintWithWriter
-  js/Symbol
-  (-pr-writer [sym writer _]
-    (-write writer (str "\"" (.toString sym) "\""))))
-
-;; doms
-(defn html-node []  js/document.documentElement)
-
-(defn ios?
-  []
-  (not (nil? (re-find #"iPad|iPhone|iPod" js/navigator.userAgent))))
-
-(defn safari?
-  []
-  (let [ua (string/lower-case js/navigator.userAgent)]
-    (and (string/includes? ua "webkit")
-         (not (string/includes? ua "chrome")))))
-
-(defn mobile?
-  []
-  (when-not node-test?
-    (re-find #"Mobi" js/navigator.userAgent)))
-
-(defn format
-  [fmt & args]
-  (apply gstring/format fmt args))
-
-(defn evalue
-  [event]
-  (gobj/getValueByKeys event "target" "value"))
-
-(defn set-change-value
-  "compatible change event for React"
-  [node value]
-  (utils/triggerInputChange node value))
-
-(defn p-handle
-  ([p ok-handler]
-   (p-handle p ok-handler (fn [error]
-                            (js/console.error error))))
-  ([p ok-handler error-handler]
-   (-> p
-       (p/then (fn [result]
-                 (ok-handler result)))
-       (p/catch (fn [error]
-                  (error-handler error))))))
-
-(defn get-width
-  []
-  (gobj/get js/window "innerWidth"))
-
-(defn indexed
-  [coll]
-  (map-indexed vector coll))
-
-(defn find-first
-  [pred coll]
-  (first (filter pred coll)))
-
-(defn dissoc-in
-  "Dissociates an entry from a nested associative structure returning a new
-  nested structure. keys is a sequence of keys. Any empty maps that result
-  will not be present in the new structure."
-  [m [k & ks :as keys]]
-  (if ks
-    (if-let [nextmap (get m k)]
-      (let [newmap (dissoc-in nextmap ks)]
-        (if (seq newmap)
-          (assoc m k newmap)
-          (dissoc m k)))
-      m)
-    (dissoc m k)))
-
-;; (defn format
-;;   [fmt & args]
-;;   (apply gstring/format fmt args))
-
-(defn json->clj
-  [json-string]
-  (-> json-string
-      (js/JSON.parse)
-      (js->clj :keywordize-keys true)))
-
-(defn remove-nils
-  "remove pairs of key-value that has nil value from a (possibly nested) map. also transform map to nil if all of its value are nil"
-  [nm]
-  (walk/postwalk
-   (fn [el]
-     (if (map? el)
-       (not-empty (into {} (remove (comp nil? second)) el))
-       el))
-   nm))
-
-(defn remove-nils-or-empty
-  [nm]
-  (walk/postwalk
-   (fn [el]
-     (if (map? el)
-       (not-empty (into {} (remove (comp #(or
-                                           (nil? %)
-                                           (and (coll? %)
-                                                (empty? %))) second)) el))
-       el))
-   nm))
-
-(defn index-by
-  [col k]
-  (->> (map (fn [entry] [(get entry k) entry])
-            col)
-       (into {})))
-
-;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
-(defn hiccup->class
-  [class]
-  (some->> (string/split class #"\.")
-           (string/join " ")
-           (string/trim)))
-
-(defn fetch-raw
-  ([url on-ok on-failed]
-   (fetch-raw url {} on-ok on-failed))
-  ([url opts on-ok on-failed]
-   (-> (js/fetch url (bean/->js opts))
-       (.then (fn [resp]
-                (if (>= (.-status resp) 400)
-                  (on-failed resp)
-                  (if (.-ok resp)
-                    (-> (.text resp)
-                        (.then bean/->clj)
-                        (.then #(on-ok %)))
-                    (on-failed resp))))))))
-
-(defn fetch
-  ([url on-ok on-failed]
-   (fetch url {} on-ok on-failed))
-  ([url opts on-ok on-failed]
-   (-> (js/fetch url (bean/->js opts))
-       (.then (fn [resp]
-                (if (>= (.-status resp) 400)
-                  (on-failed resp)
-                  (if (.-ok resp)
-                    (-> (.json resp)
-                        (.then bean/->clj)
-                        (.then #(on-ok %)))
-                    (on-failed resp))))))))
-
-(defn upload
-  [url file on-ok on-failed on-progress]
-  (let [xhr (js/XMLHttpRequest.)]
-    (.open xhr "put" url)
-    (gobj/set xhr "onload" on-ok)
-    (gobj/set xhr "onerror" on-failed)
-    (when (and (gobj/get xhr "upload")
-               on-progress)
-      (gobj/set (gobj/get xhr "upload")
-                "onprogress"
-                on-progress))
-    (.send xhr file)))
-
-(defn post
-  [url body on-ok on-failed]
-  (fetch url {:method "post"
-              :headers {:Content-Type "application/json"}
-              :body (js/JSON.stringify (clj->js body))}
-         on-ok
-         on-failed))
-
-(defn patch
-  [url body on-ok on-failed]
-  (fetch url {:method "patch"
-              :headers {:Content-Type "application/json"}
-              :body (js/JSON.stringify (clj->js body))}
-         on-ok
-         on-failed))
-
-(defn delete
-  [url on-ok on-failed]
-  (fetch url {:method "delete"
-              :headers {:Content-Type "application/json"}}
-         on-ok
-         on-failed))
-
-(defn zero-pad
-  [n]
-  (if (< n 10)
-    (str "0" n)
-    (str n)))
-
-(defn parse-int
-  [x]
-  (if (string? x)
-    (js/parseInt x)
-    x))
-
-(defn safe-parse-int
-  [x]
-  (let [result (parse-int x)]
-    (if (js/isNaN result)
-      nil
-      result)))
-
-(defn debounce
-  "Returns a function that will call f only after threshold has passed without new calls
-  to the function. Calls prep-fn on the args in a sync way, which can be used for things like
-  calling .persist on the event object to be able to access the event attributes in f"
-  ([threshold f] (debounce threshold f (constantly nil)))
-  ([threshold f prep-fn]
-   (let [t (atom nil)]
-     (fn [& args]
-       (when @t (js/clearTimeout @t))
-       (apply prep-fn args)
-       (reset! t (js/setTimeout #(do
-                                   (reset! t nil)
-                                   (apply f args))
-                                threshold))))))
-
-;; Caret
-(defn caret-range [node]
-  (let [doc (or (gobj/get node "ownerDocument")
-                (gobj/get node "document"))
-        win (or (gobj/get doc "defaultView")
-                (gobj/get doc "parentWindow"))
-        selection (.getSelection win)]
-    (if selection
-      (let [range-count (gobj/get selection "rangeCount")]
-        (when (> range-count 0)
-          (let [range (-> (.getSelection win)
-                          (.getRangeAt 0))
-                pre-caret-range (.cloneRange range)]
-            (.selectNodeContents pre-caret-range node)
-            (.setEnd pre-caret-range
-                     (gobj/get range "endContainer")
-                     (gobj/get range "endOffset"))
-            (.toString pre-caret-range))))
-      (when-let [selection (gobj/get doc "selection")]
-        (when (not= "Control" (gobj/get selection "type"))
-          (let [text-range (.createRange selection)
-                pre-caret-text-range (.createTextRange (gobj/get doc "body"))]
-            (.moveToElementText pre-caret-text-range node)
-            (.setEndPoint pre-caret-text-range "EndToEnd" text-range)
-            (gobj/get pre-caret-text-range "text")))))))
-
-(defn set-caret-pos!
-  [input pos]
-  (.setSelectionRange input pos pos))
-
-(defn get-caret-pos
-  [input]
-  (try
-    (let [pos ((gobj/get caret-pos "position") input)]
-      (set! pos -rect (.. input (getBoundingClientRect) (toJSON)))
-      (bean/->clj pos))
-    (catch js/Error e
-      (js/console.error e))))
-
-(defn minimize-html
-  [s]
-  (->> s
-       (string/split-lines)
-       (map string/trim)
-       (string/join "")))
-
-(defn stop [e]
-  (doto e (.preventDefault) (.stopPropagation)))
-
-(defn get-fragment
-  []
-  (when-let [hash js/window.location.hash]
-    (when (> (count hash) 2)
-      (-> (subs hash 1)
-          (string/split #"\?")
-          (first)))))
-
-;; (defn scroll-into-view
-;;   [element]
-;;   (let [scroll-top (gobj/get element "offsetTop")
-;;         scroll-top (if (zero? scroll-top)
-;;                      (-> (gobj/get element "parentElement")
-;;                          (gobj/get "offsetTop"))
-;;                      scroll-top)]
-;;     (prn {:scroll-top scroll-top})
-;;     (when-let [main (gdom/getElement "main-content")]
-;;       (prn {:main main})
-;;       (.scroll main #js {:top scroll-top
-;;                          ;; :behavior "smooth"
-;;                          }))))
-
-;; (defn scroll-to-element
-;;   [fragment]
-;;   (when fragment
-;;     (prn {:fragment fragment})
-;;     (when-not (string/blank? fragment)
-;;       (when-let [element (gdom/getElement fragment)]
-;;         (scroll-into-view element)))))
-
-(def speed 500)
-(def moving-frequency 15)
-
-(defn cur-doc-top []
-  (.. js/document -documentElement -scrollTop))
-
-(defn element-top [elem top]
-  (when elem
-    (if (.-offsetParent elem)
-      (let [client-top (or (.-clientTop elem) 0)
-            offset-top (.-offsetTop elem)]
-        (+ top client-top offset-top (element-top (.-offsetParent elem) top)))
-      top)))
-
-(defn scroll-to-element
-  [elem-id]
-  (when-not (re-find #"^/\d+$" elem-id)
-    (when elem-id
-      (when-let [elem (gdom/getElement elem-id)]
-        (.scroll (html-node)
-                 #js {:top (let [top (element-top elem 0)]
-                             (if (> top 68)
-                               (- top 68)
-                               top))
-                      :behavior "smooth"})))))
-
-(defn scroll-to
-  ([pos]
-   (scroll-to (html-node) pos))
-  ([node pos]
-   (.scroll node
-            #js {:top      pos
-                 :behavior "smooth"})))
-
-(defn scroll-to-top
-  []
-  (scroll-to 0))
-
-(defn url-encode
-  [string]
-  (some-> string str (js/encodeURIComponent) (.replace "+" "%20")))
-
-(defn url-decode
-  [string]
-  (some-> string str (js/decodeURIComponent)))
-
-(defn link?
-  [node]
-  (contains?
-   #{"A" "BUTTON"}
-   (gobj/get node "tagName")))
-
-(defn sup?
-  [node]
-  (contains?
-   #{"SUP"}
-   (gobj/get node "tagName")))
-
-(defn input?
-  [node]
-  (when node
-    (contains?
-     #{"INPUT" "TEXTAREA"}
-     (gobj/get node "tagName"))))
-
-(defn select?
-  [node]
-  (when node
-    (= "SELECT" (gobj/get node "tagName"))))
-
-(defn details-or-summary?
-  [node]
-  (when node
-    (contains?
-     #{"DETAILS" "SUMMARY"}
-     (gobj/get node "tagName"))))
-
-;; Debug
-(defn starts-with?
-  [s substr]
-  (string/starts-with? s substr))
-
-(defn journal?
-  [path]
-  (starts-with? path "journals/"))
-
-(defn drop-first-line
-  [s]
-  (let [lines (string/split-lines s)
-        others (some->> (next lines)
-                        (string/join "\n"))]
-    [(first lines)]))
-
-(defn distinct-by
-  [f col]
-  (reduce
-   (fn [acc x]
-     (if (some #(= (f x) (f %)) acc)
-       acc
-       (vec (conj acc x))))
-   []
-   col))
-
-(defn distinct-by-last-wins
-  [f col]
-  (reduce
-   (fn [acc x]
-     (if (some #(= (f x) (f %)) acc)
-       (mapv
-        (fn [v]
-          (if (= (f x) (f v))
-            x
-            v))
-        acc)
-       (vec (conj acc x))))
-   []
-   col))
-
-(defn get-git-owner-and-repo
-  [repo-url]
-  (take-last 2 (string/split repo-url #"/")))
-
-(defn get-textarea-height
-  [input]
-  (some-> input
-          (d/style)
-          (gobj/get "height")
-          (string/split #"\.")
-          first
-          (parse-int)))
-
-(defn get-textarea-line-height
-  [input]
-  (try
-    (some-> input
-            (d/style)
-            (gobj/get "lineHeight")
-            ;; TODO: is this cross-platform?
-            (string/replace "px" "")
-            (parse-int))
-    (catch js/Error _e
-      24)))
-
-(defn textarea-cursor-first-row?
-  [input line-height]
-  (<= (:top (get-caret-pos input)) line-height))
-
-(defn textarea-cursor-end-row?
-  [input line-height]
-  (>= (+ (:top (get-caret-pos input)) line-height)
-      (get-textarea-height input)))
-
-(defn safe-split-first [pattern s]
-  (if-let [first-index (string/index-of s pattern)]
-    [(subs s 0 first-index)
-     (subs s (+ first-index (count pattern)) (count s))]
-    [s ""]))
-
-(defn split-first [pattern s]
-  (when-let [first-index (string/index-of s pattern)]
-    [(subs s 0 first-index)
-     (subs s (+ first-index (count pattern)) (count s))]))
-
-(defn split-last [pattern s]
-  (when-let [last-index (string/last-index-of s pattern)]
-    [(subs s 0 last-index)
-     (subs s (+ last-index (count pattern)) (count s))]))
-
-(defn trim-safe
-  [s]
-  (when s
-    (string/trim s)))
-
-(defn trimr-without-newlines
-  [s]
-  (.replace s #"[ \t\r]+$" ""))
-
-(defn trim-only-newlines
-  [s]
-  (-> s
-      (.replace #"[\n]+$" "")
-      (.replace #"^[\n]+" "")))
-
-(defn triml-without-newlines
-  [s]
-  (.replace s #"^[ \t\r]+" ""))
-
-(defn concat-without-spaces
-  [left right]
-  (when (and (string? left)
-             (string? right))
-    (let [left (trimr-without-newlines left)
-          not-space? (or
-                      (string/blank? left)
-                      (= "\n" (last left)))]
-      (str left
-           (when-not not-space? " ")
-           (triml-without-newlines right)))))
-
-(defn join-newline
-  [& col]
-  (let [col (remove nil? col)]
-    (reduce (fn [acc s]
-              (if (or (= acc "") (= "\n" (last acc)))
-                (str acc s)
-                (str acc "\n"
-                     (.replace s #"^[\n]+" "")))) "" col)))
-
-;; Add documentation
-(defn replace-first [pattern s new-value]
-  (when-let [first-index (string/index-of s pattern)]
-    (str new-value (subs s (+ first-index (count pattern))))))
-
-(defn replace-last [pattern s new-value]
-  (when-let [last-index (string/last-index-of s pattern)]
-    (concat-without-spaces
-     (subs s 0 last-index)
-     new-value)))
-
-;; copy from https://stackoverflow.com/questions/18735665/how-can-i-get-the-positions-of-regex-matches-in-clojurescript
-(defn re-pos [re s]
-  (let [re (js/RegExp. (.-source re) "g")]
-    (loop [res []]
-      (if-let [m (.exec re s)]
-        (recur (conj res [(.-index m) (first m)]))
-        res))))
-
-(defn cursor-move-back [input n]
-  (let [{:keys [pos]} (get-caret-pos input)]
-    (set! (.-selectionStart input) (- pos n))
-    (set! (.-selectionEnd input) (- pos n))))
-
-(defn cursor-move-forward [input n]
-  (let [{:keys [pos]} (get-caret-pos input)]
-    (set! (.-selectionStart input) (+ pos n))
-    (set! (.-selectionEnd input) (+ pos n))))
-
-(defn move-cursor-to [input n]
-  (set! (.-selectionStart input) n)
-  (set! (.-selectionEnd input) n))
-
-(defn move-cursor-to-end
-  [input]
-  (let [pos (count (gobj/get input "value"))]
-    (move-cursor-to input pos)))
-
-;; copied from re_com
-(defn deref-or-value
-  "Takes a value or an atom
-  If it's a value, returns it
-  If it's a Reagent object that supports IDeref, returns the value inside it by derefing
-  "
-  [val-or-atom]
-  (if (satisfies? IDeref val-or-atom)
-    @val-or-atom
-    val-or-atom))
-
-;; copied from re_com
-(defn now->utc
-  "Return a goog.date.UtcDateTime based on local date/time."
-  []
-  (let [local-date-time (js/goog.date.DateTime.)]
-    (js/goog.date.UtcDateTime.
-     (.getYear local-date-time)
-     (.getMonth local-date-time)
-     (.getDate local-date-time)
-     0 0 0 0)))
-
-(defn safe-subvec [xs start end]
-  (if (or (neg? start)
-          (> end (count xs)))
-    []
-    (subvec xs start end)))
-
-(defn safe-subs
-  ([s start]
-   (let [c (count s)]
-     (safe-subs s start c)))
-  ([s start end]
-   (let [c (count s)]
-     (subs s (min c start) (min c end)))))
-
-(defn get-nodes-between-two-nodes
-  [id1 id2 class]
-  (when-let [nodes (array-seq (js/document.getElementsByClassName class))]
-    (let [id #(gobj/get % "id")
-          node-1 (gdom/getElement id1)
-          node-2 (gdom/getElement id2)
-          idx-1 (.indexOf nodes node-1)
-          idx-2 (.indexOf nodes node-2)
-          start (min idx-1 idx-2)
-          end (inc (max idx-1 idx-2))]
-      (safe-subvec (vec nodes) start end))))
-
-(defn rec-get-block-node
-  [node]
-  (if (and node (d/has-class? node "ls-block"))
-    node
-    (and node
-         (rec-get-block-node (gobj/get node "parentNode")))))
-
-(defn rec-get-blocks-container
-  [node]
-  (if (and node (d/has-class? node "blocks-container"))
-    node
-    (and node
-         (rec-get-blocks-container (gobj/get node "parentNode")))))
-
-(defn rec-get-blocks-content-section
-  [node]
-  (if (and node (d/has-class? node "content"))
-    node
-    (and node
-         (rec-get-blocks-content-section (gobj/get node "parentNode")))))
-
-;; Take the idea from https://stackoverflow.com/questions/4220478/get-all-dom-block-elements-for-selected-texts.
-;; FIXME: Note that it might not works for IE.
-(defn get-selected-nodes
-  [class-name]
-  (try
-    (when (gobj/get js/window "getSelection")
-      (let [selection (js/window.getSelection)
-            range (.getRangeAt selection 0)
-            container (-> (gobj/get range "commonAncestorContainer")
-                          (rec-get-blocks-container))
-            start-node (gobj/get range "startContainer")
-            container-nodes (array-seq (selection/getSelectedNodes container start-node))]
-        (map
-         (fn [node]
-           (if (or (= 3 (gobj/get node "nodeType"))
-                   (not (d/has-class? node class-name))) ;textnode
-             (rec-get-block-node node)
-             node))
-         container-nodes)))
-    (catch js/Error _e
-      nil)))
-
-(defn get-input-pos
-  [input]
-  (and input (.-selectionStart input)))
-
-(defn get-selected-text
-  []
-  (utils/getSelectionText))
-
-(def clear-selection! selection/clearSelection)
-
-(defn copy-to-clipboard! [s]
-  (let [el (js/document.createElement "textarea")]
-    (set! (.-value el) s)
-    (.setAttribute el "readonly" "")
-    (set! (-> el .-style .-position) "absolute")
-    (set! (-> el .-style .-left) "-9999px")
-    (js/document.body.appendChild el)
-    (.select el)
-    (js/document.execCommand "copy")
-    (js/document.body.removeChild el)))
-
-(defn take-at-most
-  [s n]
-  (if (<= (count s) n)
-    s
-    (subs s 0 n)))
-(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 "$")))
-(defn uuid-string?
-  [s]
-  (re-find exactly-uuid-pattern s))
-
-(defn extract-uuid
-  [s]
-  (re-find (re-pattern uuid-pattern) s))
-
-(defn drop-nth [n coll]
-  (keep-indexed #(if (not= %1 n) %2) coll))
-
-(defn capitalize-all [s]
-  (some->> (string/split s #" ")
-           (map string/capitalize)
-           (string/join " ")))
-
-(defn file-page?
-  [page-name]
-  (when page-name (re-find #"\." page-name)))
-
-;; Remove rum *reactions* assert
-(defn react
-  "Works in conjunction with [[reactive]] mixin. Use this function instead of `deref` inside render, and your component will subscribe to changes happening to the derefed atom."
-  [ref]
-  (when rum.core/*reactions*
-    (vswap! rum.core/*reactions* conj ref))
-  (and ref @ref))
-
-(defn time-ms
-  []
-  (tc/to-long (cljs-time.core/now)))
-
-(defn get-repo-dir
-  [repo-url]
-  (str "/"
-       (->> (take-last 2 (string/split repo-url #"/"))
-            (string/join "_"))))
-
-(defn d
-  [k f]
-  (let [result (atom nil)]
-    (println (str "Debug " k))
-    (time (reset! result (doall (f))))
-    @result))
-
-(defn concat-without-nil
-  [& cols]
-  (->> (apply concat cols)
-       (remove nil?)))
-
-(defn set-title!
-  [title]
-  (set! (.-title js/document) title))
-
-(defn get-prev-block
-  [block]
-  (when-let [blocks (d/by-class "ls-block")]
-    (when-let [index (.indexOf blocks block)]
-      (when (> index 0)
-        (nth blocks (dec index))))))
-
-(defn get-next-block
-  [block]
-  (when-let [blocks (d/by-class "ls-block")]
-    (when-let [index (.indexOf blocks block)]
-      (when (> (count blocks) (inc index))
-        (nth blocks (inc index))))))
-
-(defn get-prev-block-with-same-level
-  [block]
-  (let [id (gobj/get block "id")
-        prefix (re-find #"ls-block-[\d]+" id)]
-    (when-let [blocks (d/by-class "ls-block")]
-      (when-let [index (.indexOf blocks block)]
-        (let [level (d/attr block "level")]
-          (when (> index 0)
-            (loop [idx (dec index)]
-              (if (>= idx 0)
-                (let [block (nth blocks idx)
-                      prefix-match? (starts-with? (gobj/get block "id") prefix)]
-                  (if (and prefix-match?
-                           (= level (d/attr block "level")))
-                    block
-                    (recur (dec idx))))
-                nil))))))))
-
-(defn get-next-block-with-same-level
-  [block]
-  (when-let [blocks (d/by-class "ls-block")]
-    (when-let [index (.indexOf blocks block)]
-      (let [level (d/attr block "level")]
-        (when (> (count blocks) (inc index))
-          (loop [idx (inc index)]
-            (if (< idx (count blocks))
-              (let [block (nth blocks idx)]
-                (if (= level (d/attr block "level"))
-                  block
-                  (recur (inc idx))))
-              nil)))))))
-
-(defn get-block-idx-inside-container
-  [block-element]
-  (when block-element
-    (when-let [section (some-> (rec-get-blocks-content-section block-element)
-                               (d/parent))]
-      (let [blocks (d/by-class section "ls-block")
-            idx (when (seq blocks) (.indexOf (array-seq blocks) block-element))]
-        (when (and idx section)
-          {:idx idx
-           :container (gdom/getElement section "id")})))))
-
-(defn nth-safe [c i]
-  (if (or (< i 0) (>= i (count c)))
-    nil
-    (nth c i)))
-
-(defn sort-by-value
-  [order m]
-  (into (sorted-map-by
-         (fn [k1 k2]
-           (let [v1 (get m k1)
-                 v2 (get m k2)]
-             (if (= order :desc)
-               (compare [v2 k2] [v1 k1])
-               (compare [v1 k1] [v2 k2])))))
-        m))
-
-(defn rand-str
-  [n]
-  (-> (.toString (js/Math.random) 36)
-      (.substr 2 n)))
-
-(defn unique-id
-  []
-  (str (rand-str 6) (rand-str 3)))
-
-(defn tag-valid?
-  [tag-name]
-  (when tag-name
-    (and
-     (not (re-find #"#" tag-name))
-     (re-find regex/valid-tag-pattern tag-name))))
-
-(defn encode-str
-  [s]
-  (if (tag-valid? s)
-    s
-    (url-encode s)))
-
-(defn- get-clipboard-as-html
-  [event]
-  (if-let [c (gobj/get event "clipboardData")]
-    [(.getData c "text/html") (.getData c "text")]
-    (if-let [c (gobj/getValueByKeys event "originalEvent" "clipboardData")]
-      [(.getData c "text/html") (.getData c "text")]
-      (if-let [c (gobj/get js/window "clipboardData")]
-        [(.getData c "Text") (.getData c "Text")]))))
-
-(defn marker?
-  [s]
-  (contains?
-   #{"NOW" "LATER" "TODO" "DOING"
-     "DONE" "WAIT" "WAITING" "CANCELED" "CANCELLED" "STARTED" "IN-PROGRESS"}
-   (string/upper-case s)))
-
-(defn pp-str [x]
-  (with-out-str (pprint x)))
-
-(defn hiccup-keywordize
-  [hiccup]
-  (walk/postwalk
-   (fn [f]
-     (if (and (vector? f) (string? (first f)))
-       (update f 0 keyword)
-       f))
-   hiccup))
-
-(defn chrome?
-  []
-  (let [user-agent js/navigator.userAgent
-        vendor js/navigator.vendor]
-    (and (re-find #"Chrome" user-agent)
-         (re-find #"Google Inc" user-agent))))
-
-(defn indexeddb-check?
-  [error-handler]
-  (let [test-db "logseq-test-db-foo-bar-baz"
-        db (and js/window.indexedDB
-                (js/window.indexedDB.open test-db))]
-    (when (and db (not (chrome?)))
-      (gobj/set db "onerror" error-handler)
-      (gobj/set db "onsuccess"
-                (fn []
-                  (js/window.indexedDB.deleteDatabase test-db))))))
-
-(defonce mac? goog.userAgent/MAC)
-
-(defn ->system-modifier
-  [keyboard-shortcut]
-  (if mac?
-    (-> keyboard-shortcut
-        (string/replace "ctrl" "meta")
-        (string/replace "alt" "meta"))
-    keyboard-shortcut))
-
-(defn default-content-with-title
-  ([text-format title]
-   (default-content-with-title text-format title true))
-  ([text-format title new-block?]
-   (let [contents? (= (string/lower-case title) "contents")
-         properties (case (name text-format)
-                      "org"
-                      (format "#+TITLE: %s" title)
-                      "markdown"
-                      (format "---\ntitle: %s\n---" title)
-                      "")
-         new-block (case (name text-format)
-                     "org"
-                     "** "
-                     "markdown"
-                     "## "
-                     "")]
-     (if contents?
-       new-block
-       (str properties "\n\n" (if new-block? new-block))))))
-
-(defn get-first-block-by-id
-  [block-id]
-  (when block-id
-    (let [block-id (str block-id)]
-      (when (uuid-string? block-id)
-        (first (array-seq (js/document.getElementsByClassName block-id)))))))
-
-(defn page-name-sanity
-  [page-name]
-  (-> page-name
-      (string/replace #"\s+" "_")
-      ;; Windows reserved path characters
-      (string/replace #"[\\/:\\*\\?\"<>|]+" "_")))
-
-(defn lowercase-first
-  [s]
-  (when s
-    (str (string/lower-case (.charAt s 0))
-         (subs s 1))))
-
-(defn add-style!
-  [style]
-  (when (some? style)
-    (let [parent-node (d/sel1 :head)
-          id "logseq-custom-theme-id"
-          old-link-element (d/sel1 id)
-          style (if (string/starts-with? style "http")
-                  style
-                  (str "data:text/css;charset=utf-8," (js/encodeURIComponent style)))]
-      (when old-link-element
-        (d/remove! old-link-element))
-      (let [link (->
-                  (d/create-element :link)
-                  (d/set-attr! :id id)
-                  (d/set-attr! :rel "stylesheet")
-                  (d/set-attr! :type "text/css")
-                  (d/set-attr! :href style)
-                  (d/set-attr! :media "all"))]
-        (d/append! parent-node link)))))
-
-(defn ->platform-shortcut
-  [keyboard-shortcut]
-  (if mac?
-    (-> keyboard-shortcut
-        (string/replace "Ctrl" "Cmd")
-        (string/replace "Alt" "Opt"))
-    keyboard-shortcut))
-
-(defn remove-common-preceding
-  [col1 col2]
-  (if (and (= (first col1) (first col2))
-           (seq col1))
-    (recur (rest col1) (rest col2))
-    [col1 col2]))
-
-;; fs
-(defn get-file-ext
-  [file]
-  (last (string/split file #"\.")))
-
-(defn get-dir-and-basename
-  [path]
-  (let [parts (string/split path "/")
-        basename (last parts)
-        dir (->> (butlast parts)
-                 (string/join "/"))]
-    [dir basename]))
-
-(defn get-relative-path
-  [current-file-path another-file-path]
-  (let [directories-f #(butlast (string/split % "/"))
-        parts-1 (directories-f current-file-path)
-        parts-2 (directories-f another-file-path)
-        [parts-1 parts-2] (remove-common-preceding parts-1 parts-2)
-        another-file-name (last (string/split another-file-path "/"))]
-    (->> (concat
-          (if (seq parts-1)
-            (repeat (count parts-1) "..")
-            ["."])
-          parts-2
-          [another-file-name])
-         (string/join "/"))))
-
-;; Copy from hiccup
-(defn escape-html
-  "Change special characters into HTML character entities."
-  [text]
-  (-> text
-      (string/replace "&"  "&amp;")
-      (string/replace "<"  "&lt;")
-      (string/replace ">"  "&gt;")
-      (string/replace "\"" "&quot;")
-      (string/replace "'" "&apos;")))
-
-(defn unescape-html
-  [text]
-  (-> text
-      (string/replace "&amp;" "&")
-      (string/replace "&lt;" "<")
-      (string/replace "&gt;" ">")
-      (string/replace "&quot;" "\"")
-      (string/replace "&apos;" "'")))
-
-(comment
-  (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
-     "../pages/grant_ideas.org")
-
-  (= (get-relative-path "journals/2020_11_18.org" "journals/2020_11_19.org")
-     "./2020_11_19.org")
-
-  (= (get-relative-path "a/b/c/d/g.org" "a/b/c/e/f.org")
-     "../e/f.org"))