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