diff --git a/deps/outliner/src/logseq/outliner/op.cljs b/deps/outliner/src/logseq/outliner/op.cljs index 948a034117..70a631b669 100644 --- a/deps/outliner/src/logseq/outliner/op.cljs +++ b/deps/outliner/src/logseq/outliner/op.cljs @@ -6,6 +6,7 @@ [logseq.db :as ldb] [logseq.db.sqlite.export :as sqlite-export] [logseq.outliner.core :as outliner-core] + [logseq.outliner.template :as outliner-template] [logseq.outliner.page :as outliner-page] [logseq.outliner.property :as outliner-property] [logseq.outliner.recycle :as outliner-recycle] @@ -227,15 +228,19 @@ {:include-property-block? true}) rest)] (when (seq template-blocks) - (cons (assoc (first template-blocks) + (cons (assoc (into {} (first template-blocks)) + :db/id (:db/id (first template-blocks)) :logseq.property/used-template (:db/id template)) - (rest template-blocks)))))) + (map (fn [block] + (assoc (into {} block) :db/id (:db/id block))) + (rest template-blocks))))))) (defn- apply-template-op! [conn *result [template-id target-block-id opts]] (when-let [target (d/entity @conn target-block-id)] (let [blocks (or (some-> (:template-blocks opts) seq vec) - (template-children-blocks @conn template-id))] + (template-children-blocks @conn template-id)) + blocks (outliner-template/resolve-dynamic-template-blocks @conn target blocks)] (when (seq blocks) (let [sibling? (:sibling? opts) sibling?' (cond diff --git a/deps/outliner/src/logseq/outliner/template.cljs b/deps/outliner/src/logseq/outliner/template.cljs new file mode 100644 index 0000000000..719eab2e79 --- /dev/null +++ b/deps/outliner/src/logseq/outliner/template.cljs @@ -0,0 +1,119 @@ +(ns logseq.outliner.template + (:require [clojure.string :as string] + [datascript.core :as d] + [logseq.common.util.page-ref :as page-ref] + [logseq.db :as ldb])) + +(def ^:private template-re #"<%([^%].*?)%>") + +(defn- current-time + [] + (let [d (js/Date.) + locale (some-> js/globalThis (aget "navigator") (aget "language"))] + (.toLocaleTimeString d locale (clj->js {:hour "2-digit" + :minute "2-digit" + :hourCycle "h23"})))) + +(defn- date-with-day-offset + [offset-days] + (let [d (js/Date.)] + (.setHours d 0 0 0 0) + (.setDate d (+ (.getDate d) offset-days)) + d)) + +(defn- date->journal-day + [^js/Date date] + (let [year (.getFullYear date) + month (inc (.getMonth date)) + day (.getDate date) + month' (if (< month 10) (str "0" month) (str month)) + day' (if (< day 10) (str "0" day) (str day))] + (js/parseInt (str year month' day') 10))) + +(defn- journal-title + [db offset-days] + (let [journal-day (date->journal-day (date-with-day-offset offset-days))] + (or (d/q '[:find ?title . + :in $ ?journal-day + :where + [?p :block/journal-day ?journal-day] + [?p :block/title ?title]] + db journal-day) + (str journal-day)))) + +(defn- target-page-title + [target] + (cond + (ldb/page? target) + (:block/title target) + + :else + (get-in target [:block/page :block/title]))) + +(defn- variable-rules + [db target] + (let [today (journal-title db 0) + current-page (or (target-page-title target) today)] + {"today" (page-ref/->page-ref today) + "yesterday" (page-ref/->page-ref (journal-title db -1)) + "tomorrow" (page-ref/->page-ref (journal-title db 1)) + "time" (current-time) + "current page" (page-ref/->page-ref current-page)})) + +(defn- resolve-string + [content rules] + (string/replace content template-re + (fn [[_ match]] + (let [match' (string/trim match) + lowered (string/lower-case match')] + (cond + (string/blank? match') + "" + + (contains? rules lowered) + (or (get rules lowered) "") + + :else + match'))))) + +(defn- normalize-block + [block] + (cond-> (into {} block) + (:db/id block) + (assoc :db/id (:db/id block)))) + +(defn- resolve-field + [value rules] + (if (string? value) + (resolve-string value rules) + value)) + +(defn- resolve-properties-text-values + [value rules] + (if (map? value) + (reduce-kv (fn [m k v] + (assoc m k (resolve-field v rules))) + {} + value) + value)) + +(defn- resolve-block + [block rules] + (cond-> block + (contains? block :block/title) + (update :block/title resolve-field rules) + + (contains? block :block/raw-title) + (update :block/raw-title resolve-field rules) + + (contains? block :block/properties-text-values) + (update :block/properties-text-values resolve-properties-text-values rules))) + +(defn resolve-dynamic-template-blocks + [db target blocks] + (let [rules (variable-rules db target)] + (mapv (fn [block] + (-> block + normalize-block + (resolve-block rules))) + blocks))) diff --git a/deps/outliner/test/logseq/outliner/op_test.cljs b/deps/outliner/test/logseq/outliner/op_test.cljs index f4e53d23e2..172dc24f43 100644 --- a/deps/outliner/test/logseq/outliner/op_test.cljs +++ b/deps/outliner/test/logseq/outliner/op_test.cljs @@ -1,5 +1,6 @@ (ns logseq.outliner.op-test - (:require [cljs.test :refer [deftest is testing]] + (:require [clojure.string :as string] + [cljs.test :refer [deftest is testing]] [datascript.core :as d] [logseq.db :as ldb] [logseq.db.test.helper :as db-test] @@ -116,3 +117,50 @@ (is (= #{"page y" "page z"} (set (map :block/name (:plugin.property._test_plugin/x7 (d/entity @conn block-id))))))))) + +(deftest apply-template-op-resolves-dynamic-variables-test + (testing "apply-template resolves dynamic variables in block title and property values" + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "Target Page"} + :blocks [{:block/title "target block"}]} + {:page {:block/title "Templates"} + :blocks [{:block/title "template root" + :build/children [{:block/title "page is <% current page %>"} + {:block/title "time block" + :build/properties {:log-time "<%time%>"}}]}]}] + :properties {:log-time {:logseq.property/type :default}}}) + template-root (db-test/find-block-by-content @conn "template root") + target-block (db-test/find-block-by-content @conn "target block") + template-blocks (->> (ldb/get-block-and-children @conn (:block/uuid template-root) + {:include-property-block? true}) + rest) + blocks-to-insert (cons (assoc (into {} (first template-blocks)) + :db/id (:db/id (first template-blocks)) + :logseq.property/used-template (:db/id template-root)) + (map (fn [block] + (assoc (into {} block) :db/id (:db/id block))) + (rest template-blocks))) + _ (outliner-op/apply-ops! conn + [[:apply-template [(:db/id template-root) + (:db/id target-block) + {:template-blocks blocks-to-insert}]]] + {}) + page-var-block (db-test/find-block-by-content @conn "page is [[Target Page]]") + time-block (some->> (d/q '[:find [?b ...] + :in $ ?title ?page-title + :where + [?b :block/title ?title] + [?b :block/page ?page] + [?page :block/title ?page-title]] + @conn "time block" "Target Page") + first + (d/entity @conn)) + time-value (some (fn [[property-id value]] + (when (= "log-time" (name property-id)) + value)) + (db-test/readable-properties time-block))] + (is (some? page-var-block)) + (is (string? time-value)) + (is (not (string/blank? time-value))) + (is (not (string/includes? time-value "<%")))))) diff --git a/src/main/frontend/worker/pipeline.cljs b/src/main/frontend/worker/pipeline.cljs index 72b9bb4376..e1478d8336 100644 --- a/src/main/frontend/worker/pipeline.cljs +++ b/src/main/frontend/worker/pipeline.cljs @@ -20,6 +20,7 @@ [logseq.graph-parser.exporter :as gp-exporter] [logseq.outliner.core :as outliner-core] [logseq.outliner.datascript-report :as ds-report] + [logseq.outliner.template :as outliner-template] [logseq.outliner.pipeline :as outliner-pipeline])) (def ^:private rtc-tx-or-download-graph? @@ -67,43 +68,51 @@ journal-page (some (fn [d] (when (and (= :block/journal-day (:a d)) (:added d)) (d/entity db (:e d)))) (:tx-data tx-report)) - journal-template? (some (fn [d] (and (:added d) (= (:a d) :block/tags) (= (:v d) journal-id))) (:tx-data tx-report)) - tx-data (some->> (:tx-data tx-report) - (filter (fn [d] (and (= (:a d) :block/tags) (:added d)))) - (group-by :e) - (mapcat (fn [[e datoms]] - (let [object (d/entity db e) - template-blocks (->> (mapcat (fn [id] - (let [tag (d/entity db id) - parents (ldb/get-class-extends tag) - templates (mapcat :logseq.property/_template-applied-to (conj parents tag))] - (cond->> templates - journal-page - (map (fn [t] (assoc t :journal journal-page)))))) - (set (map :v datoms))) - distinct - (sort-by :block/created-at) - (mapcat (fn [template] - (let [template-blocks (rest (ldb/get-block-and-children db (:block/uuid template) - {:include-property-block? true})) - blocks (->> - (cons (assoc (first template-blocks) :logseq.property/used-template (:db/id template)) - (rest template-blocks)) - (map (fn [e] - (cond-> - (assoc (into {} e) :db/id (:db/id e)) - (:journal template) - (assoc :block/uuid - (common-uuid/gen-journal-template-block (:block/uuid (:journal template)) - (:block/uuid e)))))))] - blocks))))] - (when (seq template-blocks) - (let [result (outliner-core/insert-blocks - db template-blocks object - {:sibling? false - :keep-uuid? journal-template? - :outliner-op :insert-template-blocks})] - (:tx-data result)))))))] + journal-template? (some (fn [d] (and (:added d) + (= (:a d) :block/tags) + (= (:v d) journal-id))) + (:tx-data tx-report)) + tag->templates (fn [id] + (let [tag (d/entity db id) + parents (ldb/get-class-extends tag) + templates (mapcat :logseq.property/_template-applied-to (conj parents tag))] + (cond->> templates + journal-page + (map (fn [t] (assoc t :journal journal-page)))))) + template->blocks (fn [object template] + (let [template-children (rest (ldb/get-block-and-children db (:block/uuid template) + {:include-property-block? true})) + blocks (->> (cons (assoc (first template-children) + :logseq.property/used-template (:db/id template)) + (rest template-children)) + (map (fn [block] + (cond-> + (assoc (into {} block) :db/id (:db/id block)) + (:journal template) + (assoc :block/uuid + (common-uuid/gen-journal-template-block + (:block/uuid (:journal template)) + (:block/uuid block)))))))] + (outliner-template/resolve-dynamic-template-blocks db object blocks))) + tag-additions (->> (:tx-data tx-report) + (filter (fn [d] (and (= (:a d) :block/tags) (:added d)))) + (group-by :e)) + tx-data (mapcat + (fn [[e datoms]] + (let [object (d/entity db e) + templates (->> (set (map :v datoms)) + (mapcat tag->templates) + distinct + (sort-by :block/created-at)) + blocks-to-insert (mapcat (partial template->blocks object) templates)] + (when (seq blocks-to-insert) + (let [result (outliner-core/insert-blocks + db blocks-to-insert object + {:sibling? false + :keep-uuid? journal-template? + :outliner-op :insert-template-blocks})] + (:tx-data result))))) + tag-additions)] tx-data)) (defn- fix-page-tags diff --git a/src/test/frontend/worker/pipeline_test.cljs b/src/test/frontend/worker/pipeline_test.cljs index c64753063b..ce11569fdb 100644 --- a/src/test/frontend/worker/pipeline_test.cljs +++ b/src/test/frontend/worker/pipeline_test.cljs @@ -187,3 +187,26 @@ ;; return global fn back to previous behavior (ldb/register-transact-pipeline-fn! identity))) + +(deftest tag-template-insertion-resolves-dynamic-variable-test + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "Target Page"} + :blocks [{:block/title "target block"}]} + {:page {:block/title "Templates"} + :blocks [{:block/title "tag template root" + :build/children [{:block/title "auto <% current page %>"}]}]}] + :classes {:DiaryEntry {}}}) + target-block (db-test/find-block-by-content @conn "target block") + template-root (db-test/find-block-by-content @conn "tag template root") + diary-entry (ldb/get-page @conn "DiaryEntry")] + (ldb/transact! conn [[:db/add (:db/id template-root) + :logseq.property/template-applied-to + (:db/id diary-entry)]]) + (ldb/register-transact-pipeline-fn! worker-pipeline/transact-pipeline) + (try + (ldb/transact! conn [[:db/add (:db/id target-block) :block/tags (:db/id diary-entry)]]) + (is (some? (db-test/find-block-by-content @conn "auto [[Target Page]]"))) + (finally + ;; return global fn back to previous behavior + (ldb/register-transact-pipeline-fn! identity)))))