fix(template): resolve dynamic variables in template insertion

This commit is contained in:
Tienson Qin
2026-04-14 20:20:10 +08:00
parent ca6c254328
commit cfe00a5e71
5 changed files with 245 additions and 41 deletions

View File

@@ -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

View File

@@ -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)))

View File

@@ -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 "<%"))))))

View File

@@ -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

View File

@@ -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)))))