diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index d929c29d90..c982062165 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -39,6 +39,7 @@ electron.utils utils "/electron/utils" js-utils frontend.commands commands + frontend.components.block.macros block-macros frontend.components.query query frontend.components.query.result query-result frontend.config config diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 0b60a55dd7..6b30ea9fa6 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -12,6 +12,7 @@ [datascript.core :as d] [dommy.core :as dom] [frontend.commands :as commands] + [frontend.components.block.macros :as block-macros] [frontend.components.datetime :as datetime-comp] [frontend.components.lazy-editor :as lazy-editor] [frontend.components.macro :as macro] @@ -37,13 +38,11 @@ [frontend.fs :as fs] [frontend.handler.assets :as assets-handler] [frontend.handler.block :as block-handler] - [frontend.handler.common :as common-handler] [frontend.handler.dnd :as dnd] [frontend.handler.editor :as editor-handler] [frontend.handler.file-sync :as file-sync] [frontend.handler.notification :as notification] [frontend.handler.plugin :as plugin-handler] - [frontend.handler.query :as query-handler] [frontend.handler.repeated :as repeated] [frontend.handler.route :as route-handler] [frontend.handler.ui :as ui-handler] @@ -1243,17 +1242,7 @@ (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 :default e - (js/console.error e))))))) + (some-> (:query-result config) rum/react (block-macros/function-macro arguments)) [:span.warning (util/format "{{function %s}}" (first arguments))])) diff --git a/src/main/frontend/components/block/macros.cljs b/src/main/frontend/components/block/macros.cljs new file mode 100644 index 0000000000..e557c0cbd3 --- /dev/null +++ b/src/main/frontend/components/block/macros.cljs @@ -0,0 +1,67 @@ +(ns frontend.components.block.macros + "Logseq macros that render and evaluate in blocks" + (:require [clojure.walk :as walk] + [frontend.extensions.sci :as sci] + [frontend.handler.common :as common-handler] + [goog.string :as gstring] + [goog.string.format])) + +(defn- normalize-query-function + [ast result] + (let [ast (walk/prewalk + (fn [f] + (if (and (list? f) + (keyword? (second f)) + (contains? #{'sum 'average 'count 'min 'max} (first f))) + (if (contains? #{'min 'max} (first f)) + (list + 'apply + (first f) + (list 'map (second f) 'result)) + (list + (first f) + (list 'map (second f) 'result))) + f)) + ast)] + (walk/postwalk + (fn [f] + (cond + (keyword? f) + ;; These keyword aliases should be the same as those used in the query-table for sorting + (case f + :block + :block/content + + :page + :block/name + + :created-at + :block/created-at + + :updated-at + :block/updated-at + + (let [vals (map #(get-in % [:block/properties f]) result) + int? (some integer? vals)] + `(~'fn [~'b] + (~'let [~'result-str (~'get-in ~'b [:block/properties ~f]) + ~'result-num (~'parseFloat ~'result-str) + ~'result (if (~'isNaN ~'result-num) ~'result-str ~'result-num)] + (~'or ~'result (~'when ~int? 0)))))) + + :else + f)) + ast))) + +(defn function-macro + "Provides functionality for {{function}}" + [query-result arguments] + (let [fn-string (-> (gstring/format "(fn [result] %s)" (first arguments)) + (common-handler/safe-read-string "failed to parse function") + (normalize-query-function query-result) + (str)) + f (sci/eval-string fn-string)] + (when (fn? f) + (try (f query-result) + (catch :default e + (js/console.error e)))))) \ No newline at end of file diff --git a/src/main/frontend/handler/query.cljs b/src/main/frontend/handler/query.cljs deleted file mode 100644 index ab9165013d..0000000000 --- a/src/main/frontend/handler/query.cljs +++ /dev/null @@ -1,49 +0,0 @@ -(ns frontend.handler.query - "Provides util handler fns for query" - (:require [clojure.walk :as walk])) - -(defn normalize-query-function - [ast result] - (let [ast (walk/prewalk - (fn [f] - (if (and (list? f) - (keyword? (second f)) - (contains? #{'sum 'average 'count 'min 'max} (first f))) - (if (contains? #{'min 'max} (first f)) - (list - 'apply - (first f) - (list 'map (second f) 'result)) - (list - (first f) - (list 'map (second f) 'result))) - f)) - ast)] - (walk/postwalk - (fn [f] - (cond - (keyword? f) - (case f - :block - :block/content - - :page - :block/name - - :created-at - :block/created-at - - :updated-at - :block/updated-at - - (let [vals (map #(get-in % [:block/properties f]) result) - int? (some integer? vals)] - `(~'fn [~'b] - (~'let [~'result-str (~'get-in ~'b [:block/properties ~f]) - ~'result-num (~'parseFloat ~'result-str) - ~'result (if (~'isNaN ~'result-num) ~'result-str ~'result-num)] - (~'or ~'result (~'when ~int? 0)))))) - - :else - f)) - ast))) diff --git a/src/test/frontend/components/block/macros_test.cljs b/src/test/frontend/components/block/macros_test.cljs new file mode 100644 index 0000000000..04b0338b85 --- /dev/null +++ b/src/test/frontend/components/block/macros_test.cljs @@ -0,0 +1,35 @@ +(ns frontend.components.block.macros-test + (:require [frontend.components.block.macros :as block-macros] + [clojure.test :refer [deftest are testing is]])) + +(deftest macro-function + (testing "Default table functions with property argument" + (are [user-input result] + (= result + (block-macros/function-macro + (mapv #(hash-map :block/properties %) [{:total 10} {:total 20} {:total 30}]) + [user-input])) + "(sum :total)" 60 + "(average :total)" 20 + "(max :total)" 30 + "(min :total)" 10 + "(count :total)" 3)) + + (testing "Table function with clojure function argument" + (is (= 130 + (block-macros/function-macro + (mapv #(hash-map :block/properties %) + [{:total 10 :qty 3} {:total 20 :qty 5}]) + ["(sum (map (fn [x] (* (:total x) (:qty x))) result))"])))) + + (testing "Edge cases" + (is (= 40 + (block-macros/function-macro + (mapv #(hash-map :block/properties %) [{:total 10} {} {:total 30}]) + ["(sum :total)"])) + "Function still works when some results are missing property") + (is (= 0 + (block-macros/function-macro + (mapv #(hash-map :block/properties %) [{:total 10} {} {:total 30}]) + ["(sum :totally)"])) + "Function gives back 0 when given wrong property")))