(ns frontend.components.block (:refer-clojure :exclude [range]) (:require ["/frontend/utils" :as utils] [cljs-bean.core :as bean] [cljs.core.match :refer [match]] [cljs.reader :as reader] [clojure.string :as string] [clojure.walk :as walk] [datascript.core :as d] [dommy.core :as dom] [frontend.commands :as commands] [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 :refer [t]] [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] [frontend.extensions.highlight :as highlight] [frontend.extensions.latex :as latex] [frontend.extensions.sci :as sci] [frontend.extensions.pdf.assets :as pdf-assets] [frontend.extensions.zotero :as zotero] [frontend.extensions.lightbox :as lightbox] [frontend.extensions.video.youtube :as youtube] [frontend.format.block :as block] [frontend.format.mldoc :as mldoc] [frontend.components.plugins :as plugins] [frontend.handler.plugin :as plugin-handler] [frontend.handler.block :as block-handler] [frontend.handler.dnd :as dnd] [frontend.handler.editor :as editor-handler] [frontend.handler.repeated :as repeated] [frontend.handler.route :as route-handler] [frontend.handler.ui :as ui-handler] [frontend.handler.query :as query-handler] [frontend.handler.common :as common-handler] [frontend.modules.outliner.tree :as tree] [frontend.search :as search] [frontend.security :as security] [frontend.state :as state] [frontend.template :as template] [frontend.text :as text] [frontend.ui :as ui] [frontend.util :as util] [frontend.util.clock :as clock] [frontend.util.property :as property] [frontend.util.drawer :as drawer] [logseq.graph-parser.config :as gp-config] [logseq.graph-parser.util :as gp-util] [goog.dom :as gdom] [goog.object :as gobj] [lambdaisland.glogi :as log] [medley.core :as medley] [promesa.core :as p] [reitit.frontend.easy :as rfe] [rum.core :as rum] [shadow.loader :as loader] [frontend.components.query-table :as query-table] [frontend.mobile.util :as mobile-util])) (defn safe-read-string ([s] (safe-read-string s true)) ([s warn?] (try (reader/read-string s) (catch :default e (log/error :read-string-error e :string s) (when warn? [:div.warning {:title "read-string failed"} s]))))) ;; local state (defonce *dragging? (atom false)) (defonce *dragging-block (atom nil)) (defonce *drag-to-block (atom nil)) (def *move-to (atom nil)) ;; TODO: dynamic (defonce max-depth-of-links 5) (defonce *blocks-container-id (atom 0)) ;; TODO: ;; add `key` (defn- remove-nils [col] (remove nil? col)) (defn vec-cat [& args] (->> (apply concat args) remove-nils vec)) (defn ->elem ([elem items] (->elem elem nil items)) ([elem attrs items] (let [elem (keyword elem)] (if attrs (vec (cons elem (cons attrs (seq items)))) (vec (cons elem (seq items))))))) (defn- join-lines [l] (string/trim (apply str l))) (defn- string-of-url [url] (match url ["File" s] (-> (string/replace s "file://" "") ;; "file:/Users/ll/Downloads/test.pdf" is a normal org file link (string/replace "file:" "")) ["Complex" m] (let [{:keys [link protocol]} m] (if (= protocol "file") link (str protocol "://" link))))) (defn- get-file-absolute-path [config path] (let [path (string/replace path "file:" "") block-id (:block/uuid config) current-file (and block-id (:file/path (:block/file (:block/page (db/entity [:block/uuid block-id])))))] (when current-file (let [parts (string/split current-file #"/") parts-2 (string/split path #"/") current-dir (string/join "/" (drop-last 1 parts))] (cond (if util/win32? (utils/win32 path) (util/starts-with? path "/")) path (and (not (util/starts-with? path "..")) (not (util/starts-with? path "."))) (str current-dir "/" path) :else (let [parts (loop [acc [] parts (reverse parts) col (reverse parts-2)] (if (empty? col) acc (let [[part parts] (case (first col) ".." [(first parts) (rest parts)] "." ["" parts] [(first col) (rest parts)])] (recur (conj acc part) parts (rest col))))) parts (remove #(string/blank? %) parts)] (string/join "/" (reverse parts)))))))) (defonce *resizing-image? (atom false)) (rum/defcs resizable-image < (rum/local nil ::size) {:will-unmount (fn [state] (reset! *resizing-image? false) state)} [state config title src metadata full_text local?] (let [size (get state ::size)] (ui/resize-provider (ui/resize-consumer (if-not (mobile-util/native-ios?) (cond-> {:className "resize image-resize" :onSizeChanged (fn [value] (when (and (not @*resizing-image?) (some? @size) (not= value @size)) (reset! *resizing-image? true)) (reset! size value)) :onMouseUp (fn [] (when (and @size @*resizing-image?) (when-let [block-id (:block/uuid config)] (let [size (bean/->clj @size)] (editor-handler/resize-image! block-id metadata full_text size)))) (when @*resizing-image? ;; TODO: need a better way to prevent the clicking to edit current block (js/setTimeout #(reset! *resizing-image? false) 200))) :onClick (fn [e] (when @*resizing-image? (util/stop e)))} (and (:width metadata) (not (util/mobile?))) (assoc :style {:width (:width metadata)})) {}) [:div.asset-container {:key "resize-asset-container"} [:img.rounded-sm.shadow-xl.relative (merge {:loading "lazy" :src src :title title} metadata)] [:span.ctl [:a.delete {:title "Delete this image" :on-click (fn [e] (when-let [block-id (:block/uuid config)] (let [confirm-fn (ui/make-confirm-modal {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image))) :sub-title (if local? :asset/physical-delete "") :sub-checkbox? local? :on-confirm (fn [_e {:keys [close-fn sub-selected]}] (close-fn) (editor-handler/delete-asset-of-block! {:block-id block-id :local? local? :delete-local? (first sub-selected) :repo (state/get-current-repo) :href src :title title :full-text full_text}))})] (state/set-modal! confirm-fn) (util/stop e))))} svg/trash-sm] [:a.delete.ml-1 {:title "maximize image" :on-click (fn [^js e] (let [images (js/document.querySelectorAll ".asset-container img") images (to-array images) images (if-not (= (count images) 1) (let [^js _image (.closest (.-target e) ".asset-container") image (. _image querySelector "img")] (cons image (remove #(= image %) images))) images) images (for [^js it images] {:src (.-src it) :w (.-naturalWidth it) :h (.-naturalHeight it)})] (when (seq images) (lightbox/preview-images! images))))} (svg/maximize)]]])))) (rum/defc audio-cp [src] [:audio {:src src :controls true}]) (rum/defcs asset-link < rum/reactive (rum/local nil ::src) [state config title href metadata full_text] (let [src (::src state) granted? (state/sub [:nfs/user-granted? (state/get-current-repo)]) href (config/get-local-asset-absolute-path href)] (when (or granted? (util/electron?) (mobile-util/is-native-platform?)) (p/then (editor-handler/make-asset-url href) #(reset! src %))) (when @src (let [ext (util/get-file-ext @src)] (if (contains? (set (map name config/audio-formats)) ext) (audio-cp @src) (resizable-image config title @src metadata full_text true)))))) (defn ar-url->http-url [href] (string/replace href #"^ar://" (str (state/get-arweave-gateway) "/"))) ;; TODO: safe encoding asciis ;; TODO: image link to another link (defn image-link [config url href label metadata full_text] (let [metadata (if (string/blank? metadata) nil (safe-read-string metadata false)) title (second (first label))] (ui/catch-error [:span.warning full_text] (if (and (gp-config/local-asset? href) (config/local-db? (state/get-current-repo))) (asset-link config title href metadata full_text) (let [href (cond (util/starts-with? href "http") href (util/starts-with? href "ar") (ar-url->http-url href) config/publishing? (subs href 1) (= "Embed_data" (first url)) href :else (get-file-absolute-path config href))] (resizable-image config title href metadata full_text false)))))) (defn repetition-to-string [[[kind] [duration] n]] (let [kind (case kind "Dotted" "." "Plus" "+" "DoublePlus" "++")] (str kind n (string/lower-case (str (first duration)))))) (defn timestamp-to-string [{:keys [_active date time repetition wday active]}] (let [{:keys [year month day]} date {:keys [hour min]} time [open close] (if active ["<" ">"] ["[" "]"]) repetition (if repetition (str " " (repetition-to-string repetition)) "") hour (when hour (util/zero-pad hour)) min (when min (util/zero-pad min)) time (cond (and hour min) (util/format " %s:%s" hour min) hour (util/format " %s" hour) :else "")] (util/format "%s%s-%s-%s %s%s%s%s" open (str year) (util/zero-pad month) (util/zero-pad day) wday time repetition close))) (defn timestamp [{:keys [active _date _time _repetition _wday] :as t} kind] (let [prefix (case kind "Scheduled" [:i {:class "fa fa-calendar" :style {:margin-right 3.5}}] "Deadline" [:i {:class "fa fa-calendar-times-o" :style {:margin-right 3.5}}] "Date" nil "Closed" nil "Started" [:i {:class "fa fa-clock-o" :style {:margin-right 3.5}}] "Start" "From: " "Stop" "To: " nil) class (when (= kind "Closed") "line-through")] [:span.timestamp (cond-> {:active (str active)} class (assoc :class class)) prefix (timestamp-to-string t)])) (defn range [{:keys [start stop]} stopped?] [:div {:class "timestamp-range" :stopped stopped?} (timestamp start "Start") (timestamp stop "Stop")]) (declare map-inline) (declare markup-element-cp) (declare markup-elements-cp) (declare page-reference) (rum/defc page-inner "The inner div of page reference component page-name-in-block is the overridable name of the page (legacy) All page-names are sanitized except page-name-in-block" [config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label] (let [tag? (:tag? config)] [:a {:class (if tag? "tag" "page-ref") :data-ref page-name :on-mouse-down (fn [e] (util/stop e) (if (gobj/get e "shiftKey") (when-let [page-entity (db/entity [:block/name redirect-page-name])] (state/sidebar-add-block! (state/get-current-repo) (:db/id page-entity) :page {:page page-entity})) (state/pub-event! [:page/create redirect-page-name])) (when (and contents-page? (util/mobile?) (state/get-left-sidebar-open?)) (ui-handler/close-left-sidebar!)))} (if (and (coll? children) (seq children)) (for [child children] (if (= (first child) "Label") (last child) (let [{:keys [content children]} (last child) page-name (subs content 2 (- (count content) 2))] (rum/with-key (page-reference html-export? page-name (assoc config :children children) nil) page-name)))) (cond (and label (string? label) (not (string/blank? label))) ; alias label (coll? label) (->elem :span (map-inline config label)) :else (let [original-name (util/get-page-original-name page-entity) s (if (not= (util/safe-page-name-sanity-lc original-name) page-name-in-block) page-name-in-block ;; page-name-in-block might be overrided (legacy) original-name) _ (when-not page-entity (js/console.warn "page-inner's page-entity is nil, given page-name: " page-name " page-name-in-block: " page-name-in-block))] (if tag? (str "#" s) s))))])) (rum/defc page-preview-trigger [{:keys [children sidebar? tippy-position tippy-distance fixed-position? open? manual?] :as config} page-name] (let [page-name (util/page-name-sanity-lc page-name) 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) html-template (fn [] (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 (util/page-name-sanity-lc redirect-page-name)])] (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name {:redirect? false}) (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 :delay [1000, 100] :fixed-position? fixed-position? :position (or tippy-position "top") :distance (or tippy-distance 10)} children) children))) (rum/defc page-cp "Accepts {:block/name sanitized / unsanitized page-name}" [{:keys [html-export? redirect-page-name label children contents-page? preview?] :as config} page] (when-let [page-name-in-block (:block/name page)] (let [page-name-in-block (util/remove-boundary-slashes page-name-in-block) page-name (util/page-name-sanity-lc page-name-in-block) page-entity (db/entity [:block/name page-name]) redirect-page-name (or (and (= :org (state/get-preferred-format)) (:org-mode/insert-file-link? (state/get-config)) redirect-page-name) (model/get-redirect-page-name page-name (:block/alias? config))) inner (page-inner config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label)] (if (and (not (util/mobile?)) (not preview?)) (page-preview-trigger (assoc config :children inner) page-name) inner)))) (rum/defc asset-reference [config title path] (let [repo-path (config/get-repo-dir (state/get-current-repo)) full-path (if (util/absolute-path? path) path (.. util/node-path (join repo-path (config/get-local-asset-absolute-path path)))) ext-name (util/get-file-ext full-path) title-or-path (cond (string? title) title (seq title) (->elem :span (map-inline config title)) :else path)] [:div.asset-ref-wrap {:data-ext ext-name} (if (= "pdf" ext-name) [:a.asset-ref.is-pdf {:on-mouse-down (fn [e] (when-let [current (pdf-assets/inflate-asset full-path)] (util/stop e) (state/set-state! :pdf/current current)))} title-or-path] [:a.asset-ref {:target "_blank" :href full-path} title-or-path]) (case ext-name ;; https://en.wikipedia.org/wiki/HTML5_video ("mp4" "ogg" "webm" "mov") [:video {:src full-path :controls true}] nil)])) (defonce excalidraw-loaded? (atom false)) (rum/defc excalidraw < rum/reactive {:init (fn [state] (p/let [_ (loader/load :excalidraw)] (reset! excalidraw-loaded? true)) state)} [file block-uuid] (let [loaded? (rum/react excalidraw-loaded?) draw-component (when loaded? (resolve 'frontend.extensions.excalidraw/draw))] (when draw-component (draw-component {:file file :block-uuid block-uuid})))) (rum/defc page-reference < rum/reactive [html-export? s config label] (let [show-brackets? (state/show-brackets?) nested-link? (:nested-link? config) contents-page? (= "contents" (string/lower-case (str (:id config)))) block-uuid (:block/uuid config)] (if (string/ends-with? s ".excalidraw") [:div.draw {:on-click (fn [e] (.stopPropagation e))} (excalidraw s block-uuid)] [:span.page-reference {:data-ref s} (when (and (or show-brackets? nested-link?) (not html-export?) (not contents-page?)) [:span.text-gray-500.bracket "[["]) (let [s (string/trim s)] (page-cp (assoc config :label (mldoc/plain->text label) :contents-page? contents-page?) {:block/name s})) (when (and (or show-brackets? nested-link?) (not html-export?) (not contents-page?)) [:span.text-gray-500.bracket "]]"])]))) (defn- latex-environment-content [name option content] (if (= (string/lower-case name) "equation") content (util/format "\\begin%s\n%s\\end{%s}" (str "{" name "}" option) content name))) (declare blocks-container) (defn- edit-parent-block [e config] (when-not (state/editing?) (.stopPropagation e) (editor-handler/edit-block! config :max (:block/uuid config)))) (rum/defc block-embed < rum/reactive db-mixins/query [config uuid] (when-let [block (db/entity [:block/uuid uuid])] (let [blocks (db/get-paginated-blocks (state/get-current-repo) (:db/id block) {:scoped-block-id (:db/id block)})] [:div.color-level.embed-block.bg-base-2 {:style {:z-index 2} :on-double-click #(edit-parent-block % config) :on-mouse-down (fn [e] (.stopPropagation e))} [:div.px-3.pt-1.pb-2 (blocks-container blocks (assoc config :db/id (:db/id block) :id (str uuid) :embed-id uuid :embed? true :embed-parent (:block config) :ref? false))]]))) (rum/defc page-embed < rum/reactive db-mixins/query [config page-name] (let [page-name (util/page-name-sanity-lc (string/trim page-name)) current-page (state/get-current-page)] [:div.color-level.embed.embed-page.bg-base-2 {:class (when (:sidebar? config) "in-sidebar") :on-double-click #(edit-parent-block % config) :on-mouse-down #(.stopPropagation %)} [:section.flex.items-center.p-1.embed-header [:div.mr-3 svg/page] (page-cp config {:block/name page-name})] (when (and (not= (util/page-name-sanity-lc (or current-page "")) page-name) (not= (util/page-name-sanity-lc (get config :id "")) page-name)) (let [page (model/get-page page-name) blocks (db/get-paginated-blocks (state/get-current-repo) (:db/id page))] (blocks-container blocks (assoc config :db/id (:db/id page) :id page-name :embed? true :page-embed? true :ref? false))))])) (defn- get-label-text [label] (and (= 1 (count label)) (let [label (first label)] (string? (last label)) (last label)))) (defn- get-page [label] (when-let [label-text (get-label-text label)] (db/entity [:block/name (util/page-name-sanity-lc label-text)]))) (defn- macro->text [name arguments] (if (and (seq arguments) (not= arguments ["null"])) (util/format "{{{%s %s}}}" name (string/join ", " arguments)) (util/format "{{{%s}}}" name))) (declare block-content) (declare block-container) (declare block-parents) (rum/defc block-reference < rum/reactive db-mixins/query [config id label] (when (and (not (string/blank? id)) (gp-util/uuid-string? id)) (let [block-id (uuid id) block (db/pull-block block-id) block-type (keyword (get-in block [:block/properties :ls-type])) hl-type (get-in block [:block/properties :hl-type]) repo (state/get-current-repo)] (if (and block (:block/content block)) (let [title [:span {:class "block-ref"} (block-content (assoc config :block-ref? true) block nil (:block/uuid block) (:slide? config))] inner (if label (->elem :span.block-ref (map-inline config label)) title)] [:div.block-ref-wrap.inline {:data-type (name (or block-type :default)) :data-hl-type hl-type :on-mouse-down (fn [^js/MouseEvent e] (if (util/right-click? e) (state/set-state! :block-ref/context {:block (:block config) :block-ref block-id}) (when (and (or (gobj/get e "shiftKey") (not (.. e -target (closest ".blank")))) (not (util/right-click? e))) (util/stop e) (if (gobj/get e "shiftKey") (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block-ref {:block block}) (match [block-type (util/electron?)] ;; pdf annotation [:annotation true] (pdf-assets/open-block-ref! block) ;; default open block page :else (route-handler/redirect-to-page! id))))))} (if (and (not (util/mobile?)) (not (:preview? config)) (nil? block-type)) (ui/tippy {:html (fn [] [:div.tippy-wrapper.overflow-y-auto.p-4 {:style {:width 735 :text-align "left" :max-height 600}} [(block-parents config repo block-id {:indent? true}) (blocks-container (db/get-block-and-children repo block-id) (assoc config :id (str id) :preview? true))]]) :interactive true :delay [1000, 100]} inner) inner)]) [:span.warning.mr-1 {:title "Block ref invalid"} (util/format "((%s))" id)])))) (defn inline-text ([format v] (inline-text {} format v)) ([config format v] (when (string? v) (let [inline-list (mldoc/inline->edn v (mldoc/default-config format))] [:div.inline.mr-1 (map-inline config inline-list)])))) (defn- render-macro [config name arguments macro-content format] (if macro-content (let [ast (->> (mldoc/->edn macro-content (mldoc/default-config format)) (map first)) paragraph? (and (= 1 (count ast)) (= "Paragraph" (ffirst ast)))] (if (and (not paragraph?) (mldoc/block-with-title? (ffirst ast))) [:div (markup-elements-cp (assoc config :block/format format) ast)] (inline-text format macro-content))) [:span.warning {:title (str "Unsupported macro name: " name)} (macro->text name arguments)])) (rum/defc nested-link < rum/reactive [config html-export? link] (let [show-brackets? (state/show-brackets?) {:keys [content children]} link] [:span.page-reference.nested (when (and show-brackets? (not html-export?) (not (= (:id config) "contents"))) [:span.text-gray-500 "[["]) (let [page-name (subs content 2 (- (count content) 2))] (page-cp (assoc config :children children :nested-link? true) {:block/name page-name})) (when (and show-brackets? (not html-export?) (not (= (:id config) "contents"))) [:span.text-gray-500 "]]"])])) (declare custom-query) (defn- show-link? [config metadata s full-text] (let [media-formats (set (map name config/media-formats)) metadata-show (:show (safe-read-string metadata)) format (get-in config [:block :block/format])] (or (and (= :org format) (or (and (nil? metadata-show) (or (gp-config/local-asset? s) (text/media-link? media-formats s))) (true? (boolean metadata-show)))) ;; markdown (string/starts-with? (string/triml full-text) "!") ;; image http link (and (or (string/starts-with? full-text "http://") (string/starts-with? full-text "https://")) (text/media-link? media-formats s))))) (defn- relative-assets-path->absolute-path [path] (if (util/absolute-path? path) path (.. util/node-path (join (config/get-repo-dir (state/get-current-repo)) (config/get-local-asset-absolute-path path))))) (rum/defc audio-link [config url href _label metadata full_text] (if (and (gp-config/local-asset? href) (config/local-db? (state/get-current-repo))) (asset-link config nil href metadata full_text) (let [href (cond (util/starts-with? href "http") href (util/starts-with? href "ar") (ar-url->http-url href) config/publishing? (subs href 1) (= "Embed_data" (first url)) href :else (get-file-absolute-path config href))] (audio-cp href)))) (defn- media-link [config url s label metadata full_text] (let [ext (util/get-file-ext s)] (cond (contains? (set (map name config/audio-formats)) ext) (audio-link config url s label metadata full_text) (not (contains? #{"pdf" "mp4" "webm" "mov"} ext)) (image-link config url s label metadata full_text) (util/electron?) (if (= (util/get-file-ext s) "pdf") [:a.asset-ref.is-pdf {:href "javascript:void(0);" :on-mouse-down (fn [_event] (when-let [current (pdf-assets/inflate-asset s)] (state/set-state! :pdf/current current)))} (get-label-text label)] (asset-reference config label s))))) (defn- search-link-cp [config url s label title metadata full_text] (cond (string/blank? s) [:span.warning {:title "Invalid link"} full_text] (= \# (first s)) (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1)) ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html (and (= \* (first s)) (not= \* (last s))) (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1)) (text/block-ref? s) (let [id (text/get-block-ref s)] (block-reference config id label)) (not (string/includes? s ".")) (page-reference (:html-export? config) s config label) (util/url? s) (->elem :a {:href s :data-href s :target "_blank"} (map-inline config label)) (show-link? config metadata s full_text) (media-link config url s label metadata full_text) (util/electron?) (let [path (cond (string/starts-with? s "file://") (string/replace s "file://" "") (string/starts-with? s "/") s :else (relative-assets-path->absolute-path s))] (->elem :a (cond-> {:href (str "file://" path) :data-href path :target "_blank"} title (assoc :title title)) (map-inline config label))) :else (page-reference (:html-export? config) s config label))) (defn- link-cp [config html-export? link] (let [{:keys [url label title metadata full_text]} link] (match url ["Block_ref" id] (let [label* (if (seq (mldoc/plain->text label)) label nil) {:keys [link-depth]} config link-depth (or link-depth 0)] (if (> link-depth max-depth-of-links) [:p.warning.text-sm "Block ref nesting is too deep"] (block-reference (assoc config :reference? true :link-depth (inc link-depth) :block/uuid id) id label*))) ["Page_ref" page] (let [format (get-in config [:block :block/format])] (if (and (= format :org) (show-link? config nil page page) (not (contains? #{"pdf" "mp4" "ogg" "webm"} (util/get-file-ext page)))) (image-link config url page nil metadata full_text) (let [label* (if (seq (mldoc/plain->text label)) label nil)] (if (and (string? page) (string/blank? page)) [:span (util/format "[[%s]]" page)] (page-reference (:html-export? config) page config label*))))) ["Embed_data" src] (image-link config url src nil metadata full_text) ["Search" s] (search-link-cp config url s label title metadata full_text) :else (let [href (string-of-url url) [protocol path] (or (and (= "Complex" (first url)) url) (and (= "File" (first url)) ["file" (second url)]))] (cond (and (= (get-in config [:block :block/format]) :org) (= "Complex" protocol) (= (string/lower-case (:protocol path)) "id") (string? (:link path)) (gp-util/uuid-string? (:link path))) ; org mode id (let [id (uuid (:link path)) block (db/entity [:block/uuid id])] (if (:block/pre-block? block) (let [page (:block/page block)] (page-reference html-export? (:block/name page) config label)) (block-reference config (:link path) label))) (= protocol "file") (if (show-link? config metadata href full_text) (media-link config url href label metadata full_text) (let [redirect-page-name (when (string? path) (text/get-page-name path)) config (assoc config :redirect-page-name redirect-page-name) label-text (get-label-text label) page (if (string/blank? label-text) {:block/name (db/get-file-page (string/replace href "file:" "") false)} (get-page label)) show-brackets? (state/show-brackets?)] (if (and page (when-let [ext (util/get-file-ext href)] (config/mldoc-support? ext))) [:span.page-reference (when show-brackets? [:span.text-gray-500 "[["]) (page-cp config page) (when show-brackets? [:span.text-gray-500 "]]"])] (let [href* (if (util/electron?) (relative-assets-path->absolute-path href) href)] (->elem :a (cond-> {:href (str "file://" href*) :data-href href* :target "_blank"} title (assoc :title title)) (map-inline config label)))))) (show-link? config metadata href full_text) (media-link config url href label metadata full_text) (= protocol "ar") (->elem :a.external-link (cond-> {:href (ar-url->http-url href) :target "_blank"} title (assoc :title title)) (map-inline config label)) :else (->elem :a.external-link (cond-> {:href href :target "_blank"} title (assoc :title title)) (map-inline config label))))))) ;;;; Macro component render functions (defn- macro-query-cp [config arguments] [:div.dsl-query (let [query (->> (string/join ", " arguments) (string/trim))] (when-not (string/blank? query) (custom-query (assoc config :dsl-query? true) {:title (ui/tippy {:html commands/query-doc :interactive true} [:span.font-medium.px-2.py-1.query-title.text-sm.rounded-md.shadow-xs (str "Query: " query)]) :query query})))]) (defn- macro-function-cp [config arguments] (or (when (:query-result config) (when-let [query-result (rum/react (:query-result config))] (let [fn-string (-> (util/format "(fn [result] %s)" (first arguments)) (common-handler/safe-read-string "failed to parse function") (query-handler/normalize-query-function query-result) (str)) f (sci/eval-string fn-string)] (when (fn? f) (try (f query-result) (catch js/Error e (js/console.error e))))))) [:span.warning (util/format "{{function %s}}" (first arguments))])) (defn- macro-embed-cp [config arguments] (let [a (first arguments) {:keys [link-depth]} config link-depth (or link-depth 0)] (cond (nil? a) ; empty embed nil (> link-depth max-depth-of-links) [:p.warning.text-sm "Embed depth is too deep"] (and (string/starts-with? a "[[") (string/ends-with? a "]]")) (let [page-name (text/get-page-name a)] (when-not (string/blank? page-name) (page-embed (assoc config :link-depth (inc link-depth)) page-name))) (and (string/starts-with? a "((") (string/ends-with? a "))")) (when-let [s (-> (string/replace a "((" "") (string/replace "))" "") string/trim)] (when-let [id (and s (let [s (string/trim s)] (and (gp-util/uuid-string? s) (uuid s))))] (block-embed (assoc config :link-depth (inc link-depth)) id))) :else ;TODO: maybe collections? nil))) (defn- macro-vimeo-cp [_config arguments] (when-let [url (first arguments)] (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"] (when-let [vimeo-id (nth (gp-util/safe-re-find Vimeo-regex url) 5)] (when-not (string/blank? vimeo-id) (let [width (min (- (util/get-width) 96) 560) height (int (* width (/ 315 560)))] [:iframe {:allow-full-screen "allowfullscreen" :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope" :frame-border "0" :src (str "https://player.vimeo.com/video/" vimeo-id) :height height :width width}])))))) (defn- macro-bilibili-cp [_config arguments] (when-let [url (first arguments)] (let [id-regex #"https?://www\.bilibili\.com/video/([^? ]+)"] (when-let [id (cond (<= (count url) 15) url :else (last (gp-util/safe-re-find id-regex url)))] (when-not (string/blank? id) (let [width (min (- (util/get-width) 96) 560) height (int (* width (/ 315 560)))] [:iframe {:allowfullscreen true :framespacing "0" :frameborder "no" :border "0" :scrolling "no" :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1") :width width :height (max 500 height)}])))))) (defn- macro-else-cp [name config arguments] (if-let [block-uuid (:block/uuid config)] (let [format (get-in config [:block :block/format] :markdown) macro-content (or (-> (db/entity [:block/uuid block-uuid]) (:block/page) (:db/id) (db/entity) :block/properties :macros (get name)) (get (state/get-macros) name) (get (state/get-macros) (keyword name))) macro-content (cond (= (str name) "img") (case (count arguments) 1 (util/format "[:img {:src \"%s\"}]" (first arguments)) 4 (when (and (util/safe-parse-int (nth arguments 1)) (util/safe-parse-int (nth arguments 2))) (util/format "[:img.%s {:src \"%s\" :style {:width %s :height %s}}]" (nth arguments 3) (first arguments) (util/safe-parse-int (nth arguments 1)) (util/safe-parse-int (nth arguments 2)))) 3 (when (and (util/safe-parse-int (nth arguments 1)) (util/safe-parse-int (nth arguments 2))) (util/format "[:img {:src \"%s\" :style {:width %s :height %s}}]" (first arguments) (util/safe-parse-int (nth arguments 1)) (util/safe-parse-int (nth arguments 2)))) 2 (cond (util/safe-parse-int (nth arguments 1)) (util/format "[:img {:src \"%s\" :style {:width %s}}]" (first arguments) (util/safe-parse-int (nth arguments 1))) (contains? #{"left" "right" "center"} (string/lower-case (nth arguments 1))) (util/format "[:img.%s {:src \"%s\"}]" (string/lower-case (nth arguments 1)) (first arguments)) :else macro-content) macro-content) (and (seq arguments) macro-content) (block/macro-subs macro-content arguments) :else macro-content) macro-content (when macro-content (template/resolve-dynamic-template! macro-content))] (render-macro config name arguments macro-content format)) (let [macro-content (or (get (state/get-macros) name) (get (state/get-macros) (keyword name))) format (get-in config [:block :block/format] :markdown)] (render-macro config name arguments macro-content format)))) (rum/defc namespace-hierarchy-aux [config namespace children] [:ul (for [child children] [:li {:key (str "namespace-" namespace "-" (:db/id child))} (let [shorten-name (some-> (or (:block/original-name child) (:block/name child)) (string/split "/") last)] (page-cp {:label shorten-name} child)) (when (seq (:namespace/children child)) (namespace-hierarchy-aux config (:block/name child) (:namespace/children child)))])]) (rum/defc namespace-hierarchy [config namespace children] [:div.namespace [:div.font-medium.flex.flex-row.items-center.pb-2 [:span.text-sm.mr-1 "Namespace "] (page-cp config {:block/name namespace})] (namespace-hierarchy-aux config namespace children)]) (defn- macro-cp [config options] (let [{:keys [name arguments]} options arguments (if (and (>= (count arguments) 2) (and (string/starts-with? (first arguments) "[[") (string/ends-with? (last arguments) "]]"))) ; page reference (let [title (string/join ", " arguments)] [title]) arguments)] (cond (= name "query") (macro-query-cp config arguments) (= name "function") (macro-function-cp config arguments) (= name "namespace") (let [namespace (first arguments)] (when-not (string/blank? namespace) (let [namespace (string/lower-case (text/page-ref-un-brackets! namespace)) children (model/get-namespace-hierarchy (state/get-current-repo) namespace)] (namespace-hierarchy config namespace children)))) (= name "youtube") (when-let [url (first arguments)] (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"] (when-let [youtube-id (cond (== 11 (count url)) url :else (nth (gp-util/safe-re-find YouTube-regex url) 5))] (when-not (string/blank? youtube-id) (youtube/youtube-video youtube-id))))) (= name "youtube-timestamp") (when-let [timestamp (first arguments)] (when-let [seconds (youtube/parse-timestamp timestamp)] (youtube/timestamp seconds))) (= name "zotero-imported-file") (let [[item-key filename] arguments] (when (and item-key filename) [:span.ml-1 (zotero/zotero-imported-file item-key filename)])) (= name "zotero-linked-file") (when-let [path (first arguments)] [:span.ml-1 (zotero/zotero-linked-file path)]) (= name "vimeo") (macro-vimeo-cp config arguments) ;; TODO: support fullscreen mode, maybe we need a fullscreen dialog? (= name "bilibili") (macro-bilibili-cp config arguments) (contains? #{"tweet" "twitter"} name) (when-let [url (first arguments)] (let [id-regex #"/status/(\d+)"] (when-let [id (cond (<= (count url) 15) url :else (last (gp-util/safe-re-find id-regex url)))] (ui/tweet-embed id)))) (= name "embed") (macro-embed-cp config arguments) (and plugin-handler/lsp-enabled? (= name "renderer")) (when-let [block-uuid (str (:block/uuid config))] (plugins/hook-ui-slot :macro-renderer-slotted (assoc options :uuid block-uuid))) (get @macro/macros name) ((get @macro/macros name) config options) :else (macro-else-cp name config arguments)))) (defn- emphasis-cp [config kind data] (let [elem (case kind "Bold" :b "Italic" :i "Underline" :ins "Strike_through" :del "Highlight" :mark)] (->elem elem (map-inline config data)))) (defn inline [{:keys [html-export?] :as config} item] (match item [(:or "Plain" "Spaces") s] s ["Superscript" l] (->elem :sup (map-inline config l)) ["Subscript" l] (->elem :sub (map-inline config l)) ["Tag" _] (when-let [s (block/get-tag item)] (let [s (text/page-ref-un-brackets! s)] (page-cp (assoc config :tag? true) {:block/name s}))) ["Emphasis" [[kind] data]] (emphasis-cp config kind data) ["Entity" e] [:span {:dangerouslySetInnerHTML {:__html (:html e)}}] ["Latex_Fragment" [display s]] ;display can be "Displayed" or "Inline" (if html-export? (latex/html-export s false true) (latex/latex (str (d/squuid)) s false (not= display "Inline"))) [(:or "Target" "Radio_Target") s] [:a {:id s} s] ["Email" address] (let [{:keys [local_part domain]} address address (str local_part "@" domain)] [:a {:href (str "mailto:" address)} address]) ["Nested_link" link] (nested-link config html-export? link) ["Link" link] (link-cp config html-export? link) [(:or "Verbatim" "Code") s] [:code s] ["Inline_Source_Block" x] [:code (:code x)] ["Export_Snippet" "html" s] (when (not html-export?) [:span {:dangerouslySetInnerHTML {:__html s}}]) ["Inline_Hiccup" s] ;; String to hiccup (ui/catch-error [:div.warning {:title "Invalid hiccup"} s] (-> (safe-read-string s) (security/remove-javascript-links-in-href))) ["Inline_Html" s] (when (not html-export?) ;; TODO: how to remove span and only export the content of `s`? [:span {:dangerouslySetInnerHTML {:__html s}}]) [(:or "Break_Line" "Hard_Break_Line")] [:br] ["Timestamp" [(:or "Scheduled" "Deadline") _timestamp]] nil ["Timestamp" ["Date" t]] (timestamp t "Date") ["Timestamp" ["Closed" t]] (timestamp t "Closed") ["Timestamp" ["Range" t]] (range t false) ["Timestamp" ["Clock" ["Stopped" t]]] (range t true) ["Timestamp" ["Clock" ["Started" t]]] (timestamp t "Started") ["Cookie" ["Percent" n]] [:span {:class "cookie-percent"} (util/format "[d%%]" n)] ["Cookie" ["Absolute" current total]] [:span {:class "cookie-absolute"} (util/format "[%d/%d]" current total)] ["Footnote_Reference" options] (let [{:keys [name]} options encode-name (util/url-encode name)] [:sup.fn [:a {:id (str "fnr." encode-name) :class "footref" :on-click #(route-handler/jump-to-anchor! (str "fn." encode-name))} name]]) ["Macro" options] (macro-cp config options) :else "")) (rum/defc block-child [block] block) (defn- dnd-same-block? [uuid] (= (:block/uuid @*dragging-block) uuid)) (defn- bullet-drag-start [event block uuid block-id] (editor-handler/highlight-block! uuid) (.setData (gobj/get event "dataTransfer") "block-uuid" uuid) (.setData (gobj/get event "dataTransfer") "block-dom-id" block-id) (reset! *dragging? true) (reset! *dragging-block block)) (defn- bullet-on-click [e block uuid] (if (gobj/get e "shiftKey") (do (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block block) (util/stop e)) (route-handler/redirect-to-page! uuid))) (defn- toggle-block-children [_e children] (let [block-ids (map :block/uuid children)] (dorun (if (some editor-handler/collapsable? block-ids) (map editor-handler/collapse-block! block-ids) (map editor-handler/expand-block! block-ids))))) (rum/defc block-children < rum/reactive [config children collapsed?] (let [ref? (:ref? config) children (and (coll? children) (filter map? children))] (when (and (coll? children) (seq children) (not collapsed?)) (let [doc-mode? (state/sub :document/mode?)] [:div.block-children-container.flex {:style {:margin-left (if doc-mode? 18 29)}} [:div.block-children-left-border {:on-click (fn [event] (toggle-block-children event children))}] [:div.block-children.w-full {:style {:display (if collapsed? "none" "")}} (for [child children] (when (map? child) (let [child (dissoc child :block/meta) config (cond-> (-> config (assoc :block/uuid (:block/uuid child)) (dissoc :breadcrumb-show? :embed-parent)) ref? (assoc :ref-child? true))] (rum/with-key (block-container config child) (:block/uuid child)))))]])))) (defn- block-content-empty? [{:block/keys [properties title body]}] (and (or (empty? properties) (property/properties-built-in? properties)) (empty? title) (every? #(= % ["Horizontal_Rule"]) body))) (rum/defcs block-control < rum/reactive [state config block uuid block-id collapsed? *control-show? edit?] (let [doc-mode? (state/sub :document/mode?) control-show? (util/react *control-show?) ref? (:ref? config) empty-content? (block-content-empty? block)] [:div.mr-1.flex.flex-row.items-center.sm:mr-2 {:style {:height 24 :margin-top 0 :float "left"}} [:a.block-control {:id (str "control-" uuid) :on-click (fn [event] (util/stop event) (state/clear-edit!) (if ref? (state/toggle-collapsed-block! uuid) (if collapsed? (editor-handler/expand-block! uuid) (editor-handler/collapse-block! uuid))))} [:span {:class (if control-show? "control-show cursor-pointer" "control-hide")} (ui/rotating-arrow collapsed?)]] (let [bullet [:a {:on-click (fn [event] (bullet-on-click event block 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)}]]]] (cond (and (or (mobile-util/is-native-platform?) (:ui/show-empty-bullets? (state/get-config))) (not doc-mode?)) bullet (or (and empty-content? (not edit?) (not (:block/top? block)) (not (:block/bottom? block)) (not (util/react *control-show?))) (and doc-mode? (not collapsed?) (not (util/react *control-show?)))) ;; hidden [:span.bullet-container] :else bullet))])) (rum/defc dnd-separator [move-to block-content?] [:div.relative [:div.dnd-separator.absolute {:style {:left (cond-> (if (= move-to :nested) 40 20) block-content? (- 34)) :top 0 :width "100%" :z-index 3}}]]) (defn block-checkbox [block class] (let [marker (:block/marker block) [class checked?] (cond (nil? marker) nil (contains? #{"NOW" "LATER" "DOING" "IN-PROGRESS" "TODO" "WAIT" "WAITING"} marker) [class false] (= "DONE" marker) [(str class " checked") true])] (when class (ui/checkbox {:class class :style {:margin-top -2 :margin-right 5} :checked checked? :on-mouse-down (fn [e] (util/stop-propagation e)) :on-change (fn [_e] (if checked? (editor-handler/uncheck block) (editor-handler/check block)))})))) (defn list-checkbox [config checked?] (ui/checkbox {:style {:margin-right 6} :checked checked? :on-change (fn [event] (let [target (.-target event) block (:block config) item-content (.. target -nextSibling -data) item-full-content (str (if checked? "[X]" "[ ]") " " item-content) new-item-full-content (str (if checked? "[ ]" "[X]") " " item-content)] (editor-handler/toggle-list-checkbox block item-full-content new-item-full-content)))})) (defn marker-switch [{:block/keys [marker] :as block}] (when (contains? #{"NOW" "LATER" "TODO" "DOING"} marker) (let [set-marker-fn (fn [new-marker] (fn [e] (util/stop e) (editor-handler/set-marker block new-marker))) next-marker (case marker "NOW" "LATER" "LATER" "NOW" "TODO" "DOING" "DOING" "TODO")] [:a {:class (str "marker-switch block-marker " marker) :title (util/format "Change from %s to %s" marker next-marker) :on-mouse-down (set-marker-fn next-marker)} marker]))) (defn marker-cp [{:block/keys [pre-block? marker] :as _block}] (when-not pre-block? (when (contains? #{"IN-PROGRESS" "WAIT" "WAITING"} marker) [:span {:class (str "task-status block-marker " (string/lower-case marker)) :style {:margin-right 3.5}} (string/upper-case marker)]))) (rum/defc set-priority [block priority] [:div (let [priorities (sort (remove #(= priority %) ["A" "B" "C"]))] (for [p priorities] [:a.mr-2.text-base.tooltip-priority {:key (str (random-uuid)) :priority p :on-click (fn [] (editor-handler/set-priority block p))}]))]) (rum/defc priority-text [priority] [:a.opacity-50.hover:opacity-100 {:class "priority" :href (rfe/href :page {:name priority}) :style {:margin-right 3.5}} (util/format "[#%s]" (str priority))]) (defn priority-cp [{:block/keys [pre-block? priority] :as block}] (when (and (not pre-block?) priority) (ui/tippy {:interactive true :html (set-priority block priority)} (priority-text priority)))) (defn block-tags-cp [{:block/keys [pre-block? tags] :as _block}] (when (and (not pre-block?) (seq tags)) (->elem :span {:class "block-tags"} (mapv (fn [tag] (when-let [page (db/entity (:db/id tag))] (let [tag (:block/name page)] [:a.tag.mx-1 {:data-ref tag :key (str "tag-" (:db/id tag)) :href (rfe/href :page {:name tag})} (str "#" tag)]))) tags)))) (declare block-content) (defn build-block-title [config {:block/keys [title marker pre-block? properties level heading-level] :as t}] (let [config (assoc config :block t) slide? (boolean (:slide? config)) block-ref? (:block-ref? config) block-type (or (keyword (:ls-type properties)) :default) html-export? (:html-export? config) checkbox (when (and (not pre-block?) (not html-export?)) (block-checkbox t (str "mr-1 cursor"))) marker-switch (when (and (not pre-block?) (not html-export?)) (marker-switch t)) marker-cp (marker-cp t) priority (priority-cp t) tags (block-tags-cp t) bg-color (:background-color properties) heading-level (or (and heading-level (<= heading-level 6) heading-level) (and (get properties :heading) (<= level 6) level)) elem (if heading-level (keyword (str "h" heading-level (when block-ref? ".inline"))) :span.inline)] (->elem elem (merge {:data-hl-type (:hl-type properties)} (when (and marker (not (string/blank? marker)) (not= "nil" marker)) {:class (str (string/lower-case marker))}) (when bg-color {:style {:background-color bg-color :padding-left 6 :padding-right 6 :color "#FFFFFF"} :class "with-bg-color"})) (remove-nils (concat [(when-not slide? checkbox) (when-not slide? marker-switch) marker-cp priority] (if title (conj (map-inline config title) (when (and (util/electron?) (not= block-type :default)) [:a.prefix-link {:on-click #(case block-type ;; pdf annotation :annotation (pdf-assets/open-block-ref! t) (.preventDefault %))} [:span.hl-page [:strong.forbid-edit (str "P" (or (:hl-page properties) "?"))] [:label.blank " "]] (when-let [st (and (= :area (keyword (:hl-type properties))) (:hl-stamp properties))] (pdf-assets/area-display t st))])) [[:span.opacity-50 "Click here to start writing, type '/' to see all the commands."]]) [tags]))))) (rum/defc span-comma [] [:span ", "]) (rum/defc property-cp [config block k v] (let [date (and (= k :date) (date/get-locale-string (str v)))] [:div [:span.page-property-key.font-medium (name k)] [:span.mr-1 ":"] (cond (int? v) v date date (coll? v) (let [v (->> (remove string/blank? v) (filter string?)) vals (for [v-item v] (page-cp config {:block/name v-item})) elems (interpose (span-comma) vals)] (for [elem elems] (rum/with-key elem (str (random-uuid))))) (and (string? v) (util/wrapped-by-quotes? v)) (util/unquote-string v) :else (inline-text config (:block/format block) (str v)))])) (rum/defc properties-cp [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) pre-block? (:block/pre-block? block) properties (if pre-block? (let [repo (state/get-current-repo) properties (dissoc properties :title :filters) aliases (db/get-page-alias-names repo (:block/name (db/pull (:db/id (:block/page block)))))] (if (seq aliases) (if (:alias properties) (update properties :alias (fn [c] (util/distinct-by string/lower-case (concat c aliases)))) (assoc properties :alias aliases)) properties)) properties) properties-order (if pre-block? (remove #{:title :filters} properties-order) properties-order) properties (if (seq properties-order) (map (fn [k] [k (get properties k)]) properties-order) properties)] (cond (seq properties) [:div.block-properties {:class (when pre-block? "page-properties") :title (if pre-block? "Click to edit this page's properties" "Click to edit this block's properties")} (for [[k v] properties] (rum/with-key (property-cp config block k v) (str (:block/uuid block) "-" k)))] (and pre-block? properties) [:span.opacity-50 "Properties"] :else nil))) (rum/defcs timestamp-cp < rum/reactive (rum/local false ::show?) (rum/local {} ::pos) {:will-unmount (fn [state] (when-let [show? (::show? state)] (reset! show? false)) state)} [state block typ ast] (let [show? (get state ::show?)] [:div.flex.flex-col.timestamp [:div.text-sm.flex.flex-row [:div.opacity-50.font-medium.timestamp-label (str typ ": ")] [:a.opacity-80.hover:opacity-100 {:on-mouse-down (fn [e] (util/stop e) (if @show? (do (reset! show? false) (reset! commands/*current-command nil) (state/set-editor-show-date-picker! false) (state/set-timestamp-block! nil)) (do (reset! show? true) (reset! commands/*current-command typ) (state/set-editor-show-date-picker! true) (state/set-timestamp-block! {:block block :typ typ :show? show?}))))} [:span.time-start "<"] [:time (repeated/timestamp->text ast)] [:span.time-stop ">"]]] (when (true? @show?) (let [ts (repeated/timestamp->map ast)] [:div.my-4 (datetime-comp/date-picker nil nil ts)]))])) (defn- target-forbidden-edit? [target] (or (dom/has-class? target "forbid-edit") (dom/has-class? target "bullet") (dom/has-class? target "logbook") (util/link? target) (util/time? target) (util/input? target) (util/audio? target) (util/video? target) (util/details-or-summary? target) (and (util/sup? target) (dom/has-class? target "fn")) (dom/has-class? target "image-resize"))) (defn- block-content-on-mouse-down [e block block-id _content edit-input-id] (.stopPropagation e) (let [target (gobj/get e "target") button (gobj/get e "buttons") shift? (gobj/get e "shiftKey")] (when (contains? #{1 0} button) (when-not (target-forbidden-edit? target) (if (and shift? (state/get-selection-start-block)) (editor-handler/highlight-selection-area! block-id) (do (editor-handler/clear-selection!) (editor-handler/unhighlight-blocks!) (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block) cursor-range (util/caret-range (gdom/getElement block-id)) {:block/keys [content format]} block content (->> content (property/remove-built-in-properties format) (drawer/remove-logbook))] ;; save current editing block (let [{:keys [value] :as state} (editor-handler/get-state)] (editor-handler/save-block! state value)) (state/set-editing! edit-input-id content block cursor-range false))] ;; wait a while for the value of the caret range (if (util/ios?) (f) (js/setTimeout f 5)) (when block-id (state/set-selection-start-block! block-id))))))))) (rum/defc dnd-separator-wrapper < rum/reactive [block block-id slide? top? block-content?] (let [dragging? (rum/react *dragging?) drag-to-block (rum/react *drag-to-block)] (when (and (= block-id drag-to-block) dragging? (not slide?) (not (:block/pre-block? block))) (let [move-to (rum/react *move-to)] (when-not (or (and top? (not= move-to :top)) (and (not top?) (= move-to :top)) (and block-content? (not= move-to :nested)) (and (not block-content?) (seq (:block/children block)) (= move-to :nested))) (dnd-separator move-to block-content?)))))) (defn clock-summary-cp [block body] (when (and (state/enable-timetracking?) (or (= (:block/marker block) "DONE") (contains? #{"TODO" "LATER"} (:block/marker block)))) (let [summary (clock/clock-summary body true)] (when (and summary (not= summary "0m") (not (string/blank? summary))) [:div {:style {:max-width 100}} (ui/tippy {:html (fn [] (when-let [logbook (drawer/get-logbook body)] (let [clocks (->> (last logbook) (filter #(string/starts-with? % "CLOCK:")) (remove string/blank?))] [:div.p-4 [:div.font-bold.mb-2 "LOGBOOK:"] [:ul (for [clock (take 10 (reverse clocks))] [:li clock])]]))) :interactive true :delay [1000, 100]} [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}} [:a.fade-link summary]])])))) (rum/defc block-content < rum/reactive [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?] (let [{:block/keys [title body] :as block} (if (:block/title block) block (merge block (block/parse-title-and-body uuid format pre-block? content))) collapsed? (util/collapsed? block) block-ref? (:block-ref? config) block-ref-with-title? (and block-ref? (seq title)) block-type (or (:ls-type properties) :default) content (if (string? content) (string/trim content) "") mouse-down-key (if (util/ios?) :on-click :on-mouse-down ; TODO: it seems that Safari doesn't work well with on-mouse-down ) attrs (cond-> {:blockid (str uuid) :data-type (name block-type) :style {:width "100%"}} (not block-ref?) (assoc mouse-down-key (fn [e] (block-content-on-mouse-down e block block-id content edit-input-id))))] [:div.block-content.inline (cond-> {:id (str "block-content-" uuid) :on-mouse-up (fn [e] (when (and (state/in-selection-mode?) (not (string/includes? content "```")) (not (gobj/get e "shiftKey"))) ;; clear highlighted text (util/clear-selection!)))} (not slide?) (merge attrs)) [:<> [:div.flex.flex-row.justify-between [:div.flex-1 (cond (seq title) (build-block-title config block) :else nil)] (clock-summary-cp block body)] (when (seq children) (dnd-separator-wrapper block block-id slide? false true)) (when deadline (when-let [deadline-ast (block-handler/get-deadline-ast block)] (timestamp-cp block "DEADLINE" deadline-ast))) (when scheduled (when-let [scheduled-ast (block-handler/get-scheduled-ast block)] (timestamp-cp block "SCHEDULED" scheduled-ast))) (when (and (seq properties) (let [hidden? (property/properties-built-in? properties)] (not hidden?)) (not (and block-ref? (or (seq title) (seq body)))) (not (:slide? config))) (properties-cp config block)) (when (and (not block-ref-with-title?) (seq body)) [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}} ;; TODO: consistent id instead of the idx (since it could be changed later) (let [body (block/trim-break-lines! (:block/body block))] (for [[idx child] (medley/indexed body)] (when-let [block (markup-element-cp config child)] (rum/with-key (block-child block) (str uuid "-" idx)))))]) (case (:block/warning block) :multiple-blocks [:p.warning.text-sm "Full content is not displayed, Logseq doesn't support multiple unordered lists or headings in a block."] nil)]])) (rum/defc block-refs-count < rum/static [block] (let [block-refs-count (count (:block/_refs block))] (when (> block-refs-count 0) [:div [:a.open-block-ref-link.bg-base-2.text-sm.ml-2.fade-link {:title "Open block references" :style {:margin-top -1} :on-click (fn [] (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block-ref {:block block}))} block-refs-count]]))) (rum/defc block-content-or-editor < rum/reactive [config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?] (let [editor-box (get config :editor-box) editor-id (str "editor-" edit-input-id) slide? (:slide? config)] (if (and edit? editor-box) [:div.editor-wrapper {:id editor-id} (ui/catch-error (ui/block-error "Something wrong in the editor" {}) (editor-box {:block block :block-id uuid :block-parent-id block-id :format format :heading-level heading-level :on-hide (fn [value event] (when (= event :esc) (editor-handler/save-block! (editor-handler/get-state) value) (let [select? (not (string/includes? value "```"))] (editor-handler/escape-editing select?))))} edit-input-id config))] [:div.flex.flex-row.block-content-wrapper [:div.flex-1.w-full {:style {:display (if (:slide? config) "block" "flex")}} (ui/catch-error (ui/block-error "Block Render Error:" {:content (:block/content block) :section-attrs {:on-click #(state/set-editing! edit-input-id (:block/content block) block "")}}) (block-content config block edit-input-id block-id slide?))] [:div.flex.flex-row.items-center (when (and (:embed? config) (:embed-parent config)) [:a.opacity-30.hover:opacity-100.svg-small.inline {:on-mouse-down (fn [e] (util/stop e) (when-let [block (:embed-parent config)] (editor-handler/edit-block! block :max (:block/uuid block))))} svg/edit]) (block-refs-count block)]]))) (defn non-dragging? [e] (and (= (gobj/get e "buttons") 1) (not (dom/has-class? (gobj/get e "target") "bullet-container")) (not (dom/has-class? (gobj/get e "target") "bullet")) (not @*dragging?))) (rum/defc breadcrumb-fragment [config block label] (if (= block :page) ; page (when label (let [page (db/entity [:block/name (util/page-name-sanity-lc label)])] (page-cp config page))) [:a {:on-mouse-down (fn [e] (if (gobj/get e "shiftKey") (do (util/stop e) (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block-ref {:block block})) (route-handler/redirect-to-page! (:block/uuid block))))} label])) (rum/defc breadcrumb-separator [] [:span.mx-2.opacity-50 "➤"]) (defn block-parents [config repo block-id {:keys [show-page? indent? level-limit] :or {show-page? true level-limit 3}}] (let [parents (db/get-block-parents repo block-id (inc level-limit)) page (db/get-block-page repo block-id) page-name (:block/name page) page-original-name (:block/original-name page) show? (or (seq parents) show-page? page-name) parents (if (= page-name (:block/name (first parents))) (rest parents) parents) more? (> (count parents) level-limit) parents (if more? (take-last level-limit parents) parents)] (when show? (let [page-name-props (when show-page? [:page (or page-original-name page-name)]) parents-props (doall (for [{:block/keys [uuid name content] :as block} parents] (when-not name ; not page (let [{:block/keys [title body]} (block/parse-title-and-body uuid (:block/format block) (:block/pre-block? block) content) config (assoc config :block/uuid uuid)] [block (if (seq title) (->elem :span (map-inline config title)) (->elem :div (markup-elements-cp config body)))])))) breadcrumb (->> (into [] parents-props) (concat [page-name-props] (when more? [:more])) (filterv identity) (map (fn [x] (if (vector? x) (let [[block label] x] (breadcrumb-fragment config block label)) [:span.opacity-70 "⋯"]))) (interpose (breadcrumb-separator)))] [:div.block-parents.flex-row.flex-1 {:class (when (seq breadcrumb) (str (when-not (:search? config) " my-2") (when indent? " ml-4")))} breadcrumb])))) (defn- block-drag-over [event uuid top? block-id *move-to] (util/stop event) (when-not (dnd-same-block? uuid) (let [over-block (gdom/getElement block-id) rect (utils/getOffsetRect over-block) element-top (gobj/get rect "top") element-left (gobj/get rect "left") x-offset (- (.. event -pageX) element-left) cursor-top (gobj/get event "clientY") move-to-value (cond (and top? (<= (js/Math.abs (- cursor-top element-top)) 16)) :top (> x-offset 50) :nested :else :sibling)] (reset! *drag-to-block block-id) (reset! *move-to move-to-value)))) (defn- block-drag-leave [*move-to] (reset! *move-to nil)) (defn- block-drop [event uuid target-block *move-to] (util/stop event) (when-not (dnd-same-block? uuid) (let [block-uuids (state/get-selection-block-ids) lookup-refs (map (fn [id] [:block/uuid id]) block-uuids) selected (db/pull-many (state/get-current-repo) '[*] lookup-refs) blocks (if (seq selected) selected [@*dragging-block])] (dnd/move-blocks event blocks target-block @*move-to))) (reset! *dragging? false) (reset! *dragging-block nil) (reset! *drag-to-block nil) (reset! *move-to nil) (editor-handler/unhighlight-blocks!)) (defn- block-drag-end [_event *move-to] (reset! *dragging? false) (reset! *dragging-block nil) (reset! *drag-to-block nil) (reset! *move-to nil) (editor-handler/unhighlight-blocks!)) (defn- block-mouse-over [uuid e *control-show? block-id doc-mode?] (util/stop e) (when (or (model/block-collapsed? uuid) (editor-handler/collapsable? uuid {:semantic? true})) (reset! *control-show? true)) (when-let [parent (gdom/getElement block-id)] (let [node (.querySelector parent ".bullet-container")] (when doc-mode? (dom/remove-class! node "hide-inner-bullet")))) (when (and (state/in-selection-mode?) (non-dragging? e)) (util/stop e) (editor-handler/highlight-selection-area! block-id))) (defn- block-mouse-leave [e *control-show? block-id doc-mode?] (util/stop e) (reset! *control-show? false) (when doc-mode? (when-let [parent (gdom/getElement block-id)] (when-let [node (.querySelector parent ".bullet-container")] (dom/add-class! node "hide-inner-bullet")))) (when (and (non-dragging? e) (not @*resizing-image?)) (state/into-selection-mode!))) (defn- on-drag-and-mouse-attrs [block uuid top? block-id *move-to] {:on-drag-over (fn [event] (block-drag-over event uuid top? block-id *move-to)) :on-drag-leave (fn [_event] (block-drag-leave *move-to)) :on-drop (fn [event] (block-drop event uuid block *move-to)) :on-drag-end (fn [event] (block-drag-end event *move-to))}) (defn- build-refs-data-value [refs] (let [refs (model/get-page-names-by-ids (->> (map :db/id refs) (remove nil?)))] (text/build-data-value refs))) (defn- get-children-refs [children] (let [refs (atom [])] (walk/postwalk (fn [m] (when (and (map? m) (:block/refs m)) (swap! refs concat (:block/refs m))) m) children) (distinct @refs))) (defn- root-block? [config block] (and (:block? config) (util/collapsed? block) (= (:id config) (str (:block/uuid block))))) (rum/defcs ^:large-vars/cleanup-todo block-container < rum/reactive {:init (fn [state] (let [[config block] (:rum/args state) block-id (:block/uuid block)] (cond (root-block? config block) (state/set-collapsed-block! block-id false) (:ref? config) (state/set-collapsed-block! block-id (editor-handler/block-default-collapsed? block config)) :else nil) (assoc state ::control-show? (atom false)))) :should-update (fn [old-state new-state] (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed? :block/properties :block/left :block/children :block/_refs :ui/selected?] config-compare-keys [:show-cloze?] b1 (second (:rum/args old-state)) b2 (second (:rum/args new-state)) result (or (not= (select-keys b1 compare-keys) (select-keys b2 compare-keys)) (not= (select-keys (first (:rum/args old-state)) config-compare-keys) (select-keys (first (:rum/args new-state)) config-compare-keys)))] (boolean result)))} [state config {:block/keys [uuid children pre-block? top? refs heading-level level type format content] :as block}] (let [repo (state/get-current-repo) block (merge block (block/parse-title-and-body uuid format pre-block? content)) blocks-container-id (:blocks-container-id config) config (update config :block merge block) ;; Each block might have multiple queries, but we store only the first query's result config (if (nil? (:query-result config)) (assoc config :query-result (atom nil)) config) heading? (or (= type :heading) (and heading-level (<= heading-level 6))) *control-show? (get state ::control-show?) ref? (:ref? config) db-collapsed? (util/collapsed? block) collapsed? (cond (or ref? (root-block? config block)) (state/sub-collapsed uuid) :else db-collapsed?) breadcrumb-show? (:breadcrumb-show? config) slide? (boolean (:slide? config)) custom-query? (boolean (:custom-query? config)) doc-mode? (:document/mode? config) embed? (:embed? config) reference? (:reference? config) block-id (str "ls-block-" blocks-container-id "-" uuid) has-child? (boolean (and (not pre-block?) (coll? children) (seq children))) attrs (on-drag-and-mouse-attrs block uuid top? block-id *move-to) children-refs (get-children-refs children) data-refs (build-refs-data-value children-refs) data-refs-self (build-refs-data-value refs) edit-input-id (str "edit-block-" blocks-container-id "-" uuid) edit? (state/sub [:editor/editing? edit-input-id]) card? (string/includes? data-refs-self "\"card\"") review-cards? (:review-cards? config)] [:div.ls-block (cond-> {:id block-id :data-refs data-refs :data-refs-self data-refs-self :data-collapsed (and collapsed? has-child?) :class (str uuid (when pre-block? " pre-block") (when (and card? (not review-cards?)) " shadow-xl") (when (:ui/selected? block) " selected noselect")) :blockid (str uuid) :haschild (str has-child?)} level (assoc :level level) (not slide?) (merge attrs) (or reference? embed?) (assoc :data-transclude true) embed? (assoc :data-embed true) custom-query? (assoc :data-query true)) (when (and ref? breadcrumb-show?) (block-parents config repo uuid {:show-page? false :indent? true})) ;; only render this for the first block in each container (when top? (dnd-separator-wrapper block block-id slide? true false)) [:div.flex.flex-row.pr-2 {:class (if (and heading? (seq (:block/title block))) "items-baseline" "") :on-mouse-over (fn [e] (block-mouse-over uuid e *control-show? block-id doc-mode?)) :on-mouse-leave (fn [e] (block-mouse-leave e *control-show? block-id doc-mode?))} (when (not slide?) (block-control config block uuid block-id collapsed? *control-show? edit?)) (block-content-or-editor config block edit-input-id block-id heading-level edit?)] (block-children config children collapsed?) (dnd-separator-wrapper block block-id slide? false false)])) (defn divide-lists [[f & l]] (loop [l l ordered? (:ordered f) result [[f]]] (if (seq l) (let [cur (first l) cur-ordered? (:ordered cur)] (if (= ordered? cur-ordered?) (recur (rest l) cur-ordered? (update result (dec (count result)) conj cur)) (recur (rest l) cur-ordered? (conj result [cur])))) result))) (defn list-element [l] (match l [l1 & _tl] (let [{:keys [ordered name]} l1] (cond (seq name) :dl ordered :ol :else :ul)) :else :ul)) (defn list-item [config {:keys [name content checkbox items number] :as _list}] (let [content (when-not (empty? content) (match content [["Paragraph" i] & rest] (vec-cat (map-inline config i) (markup-elements-cp config rest)) :else (markup-elements-cp config content))) checked? (some? checkbox) items (when (seq items) (->elem (list-element items) (for [item items] (list-item config item))))] (cond (seq name) [:dl {:checked checked?} [:dt (map-inline config name)] (->elem :dd (vec-cat content [items]))] :else (if (nil? checkbox) (->elem :li (cond-> {:checked checked?} number (assoc :value number)) (vec-cat [(->elem :p content)] [items])) (->elem :li {:checked checked?} (vec-cat [(->elem :p (list-checkbox config checkbox) content)] [items])))))) (defn table [config {:keys [header groups col_groups]}] (let [tr (fn [elm cols] (->elem :tr (mapv (fn [col] (->elem elm {:scope "col" :class "org-left"} (map-inline config col))) cols))) tb-col-groups (try (mapv (fn [number] (let [col-elem [:col {:class "org-left"}]] (->elem :colgroup (repeat number col-elem)))) col_groups) (catch js/Error _e [])) head (when header [:thead (tr :th header)]) groups (mapv (fn [group] (->elem :tbody (mapv #(tr :td %) group))) groups)] [:div.table-wrapper {:style {:max-width (min 700 (gobj/get js/window "innerWidth"))}} (->elem :table {:class "table-auto" :border 2 :cell-spacing 0 :cell-padding 6 :rules "groups" :frame "hsides"} (vec-cat tb-col-groups (cons head groups)))])) (defn logbook-cp [log] (let [clocks (filter #(string/starts-with? % "CLOCK:") log) clocks (reverse (sort-by str clocks)) ;; TODO: diplay states change log ; states (filter #(not (string/starts-with? % "CLOCK:")) log) ] (when (seq clocks) (let [tr (fn [elm cols] (->elem :tr (mapv (fn [col] (->elem elm col)) cols))) head [:thead.overflow-x-scroll (tr :th.py-0 ["Type" "Start" "End" "Span"])] clock-tbody (->elem :tbody.overflow-scroll.sm:overflow-auto (mapv (fn [clock] (let [cols (->> (string/split clock #": |--|=>") (map string/trim))] (mapv #(tr :td.py-0 %) [cols]))) clocks))] [:div.overflow-x-scroll.sm:overflow-auto (->elem :table.m-0 {:class "logbook-table" :border 0 :style {:width "max-content"} :cell-spacing 15} (cons head [clock-tbody]))])))) (defn map-inline [config col] (map #(inline config %) col)) (declare ->hiccup) (defn built-in-custom-query? [title] (let [repo (state/get-current-repo) queries (state/sub [:config repo :default-queries :journals])] (when (seq queries) (boolean (some #(= % title) (map :title queries)))))) (defn- trigger-custom-query! [state] (let [[config query] (:rum/args state) repo (state/get-current-repo) result-atom (atom nil) query-atom (if (:dsl-query? config) (let [q (:query query) form (safe-read-string q false)] (cond ;; Searches like 'foo' or 'foo bar' come back as symbols ;; and are meant to go directly to full text search (and (util/electron?) (symbol? form)) ; full-text search (p/let [blocks (search/block-search repo (string/trim (str form)) {:limit 30})] (when (seq blocks) (let [result (db/pull-many (state/get-current-repo) '[*] (map (fn [b] [:block/uuid (uuid (:block/uuid b))]) blocks))] (reset! result-atom result)))) (symbol? form) (atom nil) :else (query-dsl/query (state/get-current-repo) q))) (db/custom-query query)) query-atom (if (instance? Atom query-atom) query-atom result-atom)] (assoc state :query-atom query-atom))) (rum/defcs ^:large-vars/cleanup-todo custom-query* < rum/reactive {:will-mount trigger-custom-query! :did-mount (fn [state] (when-let [query (last (:rum/args state))] (state/add-custom-query-component! query (:rum/react-component state))) state) :did-remount (fn [_old_state state] (trigger-custom-query! state)) :will-unmount (fn [state] (when-let [query (last (:rum/args state))] (state/remove-custom-query-component! query) (db/remove-custom-query! (state/get-current-repo) query)) state)} [state config {:keys [title query view collapsed? children? breadcrumb-show?] :as q}] (let [dsl-query? (:dsl-query? config) query-atom (:query-atom state) current-block-uuid (or (:block/uuid (:block config)) (:block/uuid config)) current-block (db/entity [:block/uuid current-block-uuid]) ;; exclude the current one, otherwise it'll loop forever remove-blocks (if current-block-uuid [current-block-uuid] nil) 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? (boolean (:result-transform q)) (and (string? query) (string/includes? query "(by-page false)"))) 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))) view-f (and view (sci/eval-string (pr-str view))) only-blocks? (:block/uuid (first result)) blocks-grouped-by-page? (and (seq result) (not not-grouped-by-page?) (coll? (first result)) (:block/name (ffirst result)) (:block/uuid (first (second (first result)))) true) built-in? (built-in-custom-query? title) page-list? (and (seq result) (:block/name (first result))) nested-query? (:custom-query? config)] (if nested-query? [:code (if dsl-query? (util/format "{{query %s}}" query) "{{query hidden}}")] [:div.custom-query.mt-4 (get config :attr {}) (when-not (and built-in? (empty? result)) (ui/foldable [:div.custom-query-title [:span.title-text (cond (vector? title) title (string? title) (inline-text config (get-in config [:block :block/format] :markdown) title) :else title)] [:span.opacity-60.text-sm.ml-2.results-count (str (count transformed-query-result) " results")]] (fn [] [:div (when current-block [:div.flex.flex-row.align-items.mt-2 {:on-mouse-down (fn [e] (util/stop e))} (when-not page-list? [:div.flex.flex-row [:div.mx-2 [:span.text-sm "Table view"]] [:div {:style {:margin-top 5}} (ui/toggle table? (fn [] (editor-handler/set-block-property! current-block-uuid "query-table" (not table?))) true)]]) [:a.mx-2.block.fade-link {:on-click (fn [] (let [all-keys (query-table/get-keys result page-list?)] (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? (if (some? breadcrumb-show?) 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"])]) {:default-collapsed? collapsed? :title-trigger? true}))]))) (rum/defc custom-query [config q] (ui/catch-error (ui/block-error "Query Error:" {:content (:query q)}) (custom-query* config q))) (defn admonition [config type result] (when-let [icon (case (string/lower-case (name type)) "note" svg/note "tip" svg/tip "important" svg/important "caution" svg/caution "warning" svg/warning "pinned" svg/pinned nil)] [:div.flex.flex-row.admonitionblock.align-items {:class type} [:div.pr-4.admonition-icon.flex.flex-col.justify-center {:title (string/upper-case type)} (icon)] [:div.ml-4.text-lg (markup-elements-cp config result)]])) ;; TODO: move to mldoc ;; (defn- convert-md-src-to-custom-block ;; [item] ;; (let [{:keys [language options lines] :as options} (second item) ;; lang (string/lower-case (or language ""))] ;; (cond ;; (= lang "quote") ;; (let [content (string/trim (string/join "\n" lines))] ;; ["Quote" (first (mldoc/->edn content (mldoc/default-config :markdown)))]) ;; (contains? #{"query" "note" "tip" "important" "caution" "warning" "pinned"} lang) ;; (let [content (string/trim (string/join "\n" lines))] ;; ["Custom" lang nil (first (mldoc/->edn content (mldoc/default-config :markdown))) content]) ;; :else ;; ["Src" options]))) (rum/defc src-cp < rum/static [config options html-export?] (when options (let [{:keys [lines language]} options attr (when language {:data-lang language}) code (apply str lines)] (cond html-export? (highlight/html-export attr code) :else (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)] (if (:slide? config) (highlight/highlight (str (medley/random-uuid)) {:class (str "language-" language) :data-lang language} code) [:div (lazy-editor/editor config (str (d/squuid)) attr code options) (let [options (:options options)] (when (and (= language "clojure") (contains? (set options) ":results")) (sci/eval-result code)))])))))) (defn ^:large-vars/cleanup-todo markup-element-cp [{:keys [html-export?] :as config} item] (let [format (or (:block/format config) :markdown)] (try (match item ["Drawer" name lines] (when (or (not= name "logbook") (and (= name "logbook") (state/enable-timetracking?) (or (get-in (state/get-config) [:logbook/settings :enabled-in-all-blocks]) (when (get-in (state/get-config) [:logbook/settings :enabled-in-timestamped-blocks] true) (or (:block/scheduled (:block config)) (:block/deadline (:block config))))))) [:div [:div.text-sm [:div.drawer {:data-drawer-name name} (ui/foldable [:div.opacity-50.font-medium.logbook (util/format ":%s:" (string/upper-case name))] [:div.opacity-50.font-medium (if (= name "logbook") (logbook-cp lines) (apply str lines)) [:div ":END:"]] {:default-collapsed? true :title-trigger? true})]]]) ["Properties" m] [:div.properties (for [[k v] (dissoc m :roam_alias :roam_tags)] (when (and (not (and (= k :macros) (empty? v))) ; empty macros (not (= k :title)) (not (= k :filters))) [:div.property [:span.font-medium.mr-1 (str (name k) ": ")] (if (coll? v) (let [vals (for [item v] (if (coll? v) (let [config (when (= k :alias) (assoc config :block/alias? true))] (page-cp config {:block/name item})) (inline-text format item)))] (interpose [:span ", "] vals)) (inline-text format v))]))] ;; for file-level property in orgmode: #+key: value ;; only display caption. https://orgmode.org/manual/Captions.html. ["Directive" key value] [:div.file-level-property (when (contains? #{"caption"} (string/lower-case key)) [:span.font-medium [:span.font-bold (string/upper-case key)] (str ": " value)])] ["Paragraph" l] ;; TODO: speedup (if (gp-util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l)) (->elem :div (map-inline config l)) (->elem :div.is-paragraph (map-inline config l))) ["Horizontal_Rule"] (when-not (:slide? config) [:hr]) ["Heading" h] (block-container config h) ["List" l] (let [lists (divide-lists l)] (if (= 1 (count lists)) (let [l (first lists)] (->elem (list-element l) (map #(list-item config %) l))) [:div.list-group (for [l lists] (->elem (list-element l) (map #(list-item config %) l)))])) ["Table" t] (table config t) ["Math" s] (if html-export? (latex/html-export s true true) (latex/latex (str (d/squuid)) s true true)) ["Example" l] [:pre.pre-wrap-white-space (join-lines l)] ["Quote" l] (->elem :blockquote (markup-elements-cp config l)) ["Raw_Html" content] (when (not html-export?) [:div.raw_html {:dangerouslySetInnerHTML {:__html content}}]) ["Export" "html" _options content] (when (not html-export?) [:div.export_html {:dangerouslySetInnerHTML {:__html content}}]) ["Hiccup" content] (ui/catch-error [:div.warning {:title "Invalid hiccup"} content] (-> (safe-read-string content) (security/remove-javascript-links-in-href))) ["Export" "latex" _options content] (if html-export? (latex/html-export content true false) (latex/latex (str (d/squuid)) content true false)) ["Custom" "query" _options _result content] (try (let [query (reader/read-string content)] (custom-query config query)) (catch :default e (log/error :read-string-error e) (ui/block-error "Invalid query:" {:content content}))) ["Custom" "note" _options result _content] (admonition config "note" result) ["Custom" "tip" _options result _content] (admonition config "tip" result) ["Custom" "important" _options result _content] (admonition config "important" result) ["Custom" "caution" _options result _content] (admonition config "caution" result) ["Custom" "warning" _options result _content] (admonition config "warning" result) ["Custom" "pinned" _options result _content] (admonition config "pinned" result) ["Custom" "center" _options l _content] (->elem :div.text-center (markup-elements-cp config l)) ["Custom" name _options l _content] (->elem :div {:class name} (markup-elements-cp config l)) ["Latex_Fragment" l] [:p.latex-fragment (inline config ["Latex_Fragment" l])] ["Latex_Environment" name option content] (let [content (latex-environment-content name option content)] (if html-export? (latex/html-export content true true) (latex/latex (str (d/squuid)) content true true))) ["Displayed_Math" content] (if html-export? (latex/html-export content true true) (latex/latex (str (d/squuid)) content true true)) ["Footnote_Definition" name definition] (let [id (util/url-encode name)] [:div.footdef [:div.footpara (conj (markup-element-cp config ["Paragraph" definition]) [:a.ml-1 {:id (str "fn." id) :style {:font-size 14} :class "footnum" :on-click #(route-handler/jump-to-anchor! (str "fnr." id))} [:sup.fn (str name "↩︎")]])]]) ["Src" options] [:div.cp__fenced-code-block (if-let [opts (plugin-handler/hook-fenced-code-by-type (util/safe-lower-case (:language options)))] (plugins/hook-ui-fenced-code (string/join "" (:lines options)) opts) (src-cp config options html-export?))] :else "") (catch js/Error e (println "Convert to html failed, error: " e) "")))) (defn markup-elements-cp [config col] (map #(markup-element-cp config %) col)) (defn- block-item [config blocks idx item] (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) (str (:block/uuid item))))) (defn- block-list [config blocks] (for [[idx item] (medley/indexed blocks)] (block-item config blocks idx item))) (defn- custom-query-or-ref? [config] (let [ref? (:ref? config) custom-query? (:custom-query? config)] (or custom-query? ref?))) (defn- load-more-blocks! [config flat-blocks] (when-let [db-id (:db/id config)] (let [last-block-id (:db/id (last flat-blocks))] (block-handler/load-more! db-id last-block-id)))) (rum/defcs lazy-blocks < rum/reactive {:init (fn [state] (assoc state ::id (str (random-uuid))))} [state config flat-blocks blocks->vec-tree] (let [db-id (:db/id config) selected-blocks (set (state/get-selection-block-ids)) flat-blocks (if (seq selected-blocks) (map (fn [b] (assoc b :ui/selected? (contains? selected-blocks (:block/uuid b)))) flat-blocks) flat-blocks) blocks (blocks->vec-tree flat-blocks)] (if-not db-id (block-list config blocks) (let [bottom-reached (fn [] ;; To prevent scrolling after inserting new blocks (when (> (- (util/time-ms) (:start-time config)) 100) (load-more-blocks! config flat-blocks))) has-more? (and (> (count flat-blocks) model/initial-blocks-length) (some? (model/get-next-open-block (db/get-db) (last flat-blocks) db-id))) dom-id (str "lazy-blocks-" (::id state))] [:div {:id dom-id} (ui/infinite-list "main-content-container" (block-list config blocks) {:on-load bottom-reached :bottom-reached (fn [] (when-let [node (gdom/getElement dom-id)] (ui/bottom-reached? node 1000))) :has-more has-more? :more (if (or (:preview? config) (:sidebar? config)) "More" (ui/loading "Loading"))})])))) (rum/defcs blocks-container < {:init (fn [state] (assoc state ::init-blocks-container-id (atom nil)))} [state blocks config] (let [*init-blocks-container-id (::init-blocks-container-id state) blocks-container-id (if @*init-blocks-container-id @*init-blocks-container-id (let [id' (swap! *blocks-container-id inc)] (reset! *init-blocks-container-id id') id')) config (assoc config :blocks-container-id blocks-container-id) doc-mode? (:document/mode? config)] (when (seq blocks) (let [blocks->vec-tree #(if (custom-query-or-ref? config) % (tree/blocks->vec-tree % (:id config))) flat-blocks (vec blocks) config (assoc config :start-time (util/time-ms))] [:div.blocks-container.flex-1 {:class (when doc-mode? "document-mode")} (lazy-blocks config flat-blocks blocks->vec-tree)])))) ;; headers to hiccup (defn ->hiccup [blocks config option] [:div.content (cond-> option (:document/mode? config) (assoc :class "doc-mode")) (cond (and (:custom-query? config) (:group-by-page? config)) [:div.flex.flex-col (let [blocks (sort-by (comp :block/journal-day first) > blocks)] (for [[page blocks] blocks] (let [alias? (:block/alias? page) page (db/entity (:db/id page)) parent-blocks (group-by :block/parent blocks)] [:div.my-2 (cond-> {:key (str "page-" (:db/id page))} (:ref? config) (assoc :class "color-level px-2 sm:px-7 py-2 rounded")) (ui/foldable [:div (page-cp config page) (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])] (for [[_parent blocks] parent-blocks] (let [block (first blocks)] [:div (when (:breadcrumb-show? config) (block-parents config (state/get-current-repo) (:block/uuid block) {:show-page? false})) (blocks-container blocks (assoc config :breadcrumb-show? false))])) {})])))] (and (:group-by-page? config) (vector? (first blocks))) [:div.flex.flex-col (let [blocks (sort-by (comp :block/journal-day first) > blocks)] (for [[page blocks] blocks] (let [alias? (:block/alias? page) page (db/entity (:db/id page))] [:div.my-2 (cond-> {:key (str "page-" (:db/id page))} (:ref? config) (assoc :class "color-level px-2 sm:px-7 py-2 rounded")) (ui/foldable [:div (page-cp config page) (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])] (blocks-container blocks config) {})])))] :else (blocks-container blocks config))])