Merge pull request #6336 from logseq/feat/config-for-property-value-links-and-text

Feat: Config option to allow for longer, richer property values
pull/6461/head
Gabriel Horner 2022-08-23 10:08:00 -04:00 committed by GitHub
commit 0a66ecddfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 347 additions and 57 deletions

View File

@ -72,7 +72,8 @@ frontend.util/trace!
frontend.util.pool/terminate-pool!
;; Repl fn
frontend.util.property/add-page-properties
;; Test runner used by shadow
;; Test runners used by shadow
frontend.test.node-test-runner/main
frontend.test.frontend-node-test-runner/main
;; Test runner for nbb
logseq.graph-parser.nbb-test-runner/run-tests

View File

@ -18,3 +18,7 @@ logseq.graph-parser.util.page-ref/left-and-right-brackets
logseq.graph-parser.util.page-ref/->page-ref
;; API
logseq.graph-parser.util.page-ref/get-page-name!
;; API
logseq.graph-parser.property/->block-content
;; API
logseq.graph-parser.property/property-value-from-content

View File

@ -157,6 +157,8 @@
distinct)
[]))
;; TODO: Use text/parse-property to determine refs rather than maintain this similar
;; implementation to parse-property
(defn- get-page-ref-names-from-properties
[format properties user-config]
(let [page-refs (->>
@ -174,7 +176,11 @@
(and (string? v)
(not (gp-mldoc/link? format v)))
(let [v (string/trim v)
result (text/split-page-refs-without-brackets v {:un-brackets? false})]
result (if (:rich-property-values? user-config)
(if (gp-util/wrapped-by-quotes? v)
[]
(text/extract-page-refs-and-tags v))
(text/split-page-refs-without-brackets v {:un-brackets? false}))]
(if (coll? result)
(map text/page-ref-un-brackets! result)
[]))

View File

@ -44,7 +44,8 @@ TODO: Fail fast when process exits 1"
(defn- parse-files
[conn files {:keys [config] :as options}]
(let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)}
(let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)
:user-config config}
(select-keys options [:verbose]))]
(mapv
(fn [{:file/keys [path content]}]

View File

@ -1,7 +1,6 @@
(ns logseq.graph-parser.config
"Config that is shared between graph-parser and rest of app"
(:require [logseq.graph-parser.util :as gp-util]
[clojure.set :as set]
(:require [clojure.set :as set]
[clojure.string :as string]))
(def app-name
@ -12,7 +11,8 @@
(defn local-asset?
[s]
(gp-util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
(and (string? s)
(re-find (re-pattern (str "^[./]*" local-assets-dir)) s)))
(defonce default-draw-directory "draws")

View File

@ -154,7 +154,8 @@
(remove string/blank?)))
tags (:tags properties)
tags (->> (->vec-concat tags filetags)
(remove string/blank?))
(remove string/blank?)
vec)
properties (assoc properties :tags tags :alias alias)
properties (-> properties
(update :filetags (constantly filetags)))

View File

@ -8,6 +8,19 @@
(def colons "Property delimiter for markdown mode" "::")
(defn ->block-content
"Creates a block content string from properties map"
[properties]
(->> properties
(map #(str (name (key %)) (str colons " ") (val %)))
(string/join "\n")))
(defn property-value-from-content
"Extracts full property value from block content"
[property content]
(second (re-find (re-pattern (str property colons "\\s+(.*)"))
content)))
(defn properties-ast?
[block]
(and
@ -60,7 +73,7 @@
[content]
(when content
(and (string/includes? content properties-start)
(gp-util/safe-re-find properties-end-pattern content))))
(re-find properties-end-pattern content))))
(defn ->new-properties
"New syntax: key:: value"

View File

@ -5,6 +5,7 @@
[clojure.set :as set]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.util :as gp-util]
[logseq.graph-parser.property :as gp-property]
[logseq.graph-parser.util.page-ref :as page-ref :refer [right-brackets]]))
(defn get-file-basename
@ -109,8 +110,8 @@
(and (string? s)
;; Either a page ref, a tag or a comma separated collection
(or (gp-util/safe-re-find page-ref/page-ref-re s)
(gp-util/safe-re-find #"[\,||#|\"]+" s)))
(or (re-find page-ref/page-ref-re s)
(re-find #"[\,||#|\"]+" s)))
(let [result (->> (sep-by-quotes s)
(mapcat
(fn [s]
@ -199,7 +200,35 @@
(defonce non-parsing-properties
(atom #{"background-color" "background_color"}))
(defn parse-non-string-property-value
"Return parsed non-string property value or nil if none is found"
[v]
(cond
(= v "true")
true
(= v "false")
false
(re-find #"^\d+$" v)
(parse-long v)))
(def ^:private page-ref-or-tag-re
(re-pattern (str "#?" (page-ref/->page-ref-re-str "(.*?)") "|"
;; Don't capture punctuation at end of a tag
"#([\\S]+[^\\s.!,])")))
(defn extract-page-refs-and-tags
"Returns set of page-refs and tags in given string or returns string if none
are found"
[string]
(let [refs (map #(or (second %) (get % 2))
(re-seq page-ref-or-tag-re string))]
(if (seq refs) (set refs) string)))
(defn parse-property
"Property value parsing that takes into account built-in properties, format
and user config"
([k v config-state]
(parse-property :markdown k v config-state))
([format k v config-state]
@ -212,14 +241,6 @@
(get config-state :ignored-page-references-keywords)) k)
v
(= v "true")
true
(= v "false")
false
(and (not= k "alias") (gp-util/safe-re-find #"^\d+$" v))
(parse-long v)
(gp-util/wrapped-by-quotes? v) ; wrapped in ""
v
@ -229,5 +250,12 @@
(gp-mldoc/link? format v)
v
(contains? gp-property/editable-linkable-built-in-properties (keyword k))
(split-page-refs-without-brackets v)
:else
(split-page-refs-without-brackets v)))))
(if-some [res (parse-non-string-property-value v)]
res
(if (:rich-property-values? config-state)
(extract-page-refs-and-tags v)
(split-page-refs-without-brackets v)))))))

View File

@ -4,12 +4,6 @@
(:require [clojure.walk :as walk]
[clojure.string :as string]))
(defn safe-re-find
"Copy of frontend.util/safe-re-find. Too basic to couple to main app"
[pattern s]
(when (string? s)
(re-find pattern s)))
(defn path-normalize
"Normalize file path (for reading paths from FS, not required by writting)"
[s]
@ -38,7 +32,7 @@
(defn tag-valid?
[tag-name]
(when (string? tag-name)
(not (safe-re-find #"[# \t\r\n]+" tag-name))))
(not (re-find #"[# \t\r\n]+" tag-name))))
(defn safe-subs
([s start]

View File

@ -27,6 +27,11 @@ a logseq page-ref e.g. [[page name]]"
[page-name]
(str left-brackets page-name right-brackets))
(defn ->page-ref-re-str
"Create a page ref regex escaped string given a page name"
[page-name]
(string/replace (->page-ref page-name) #"([\[\]])" "\\$1"))
(defn get-page-name
"Extracts page-name from page-ref string"
[s]

View File

@ -1,5 +1,5 @@
(ns logseq.graph-parser.property-test
(:require [cljs.test :refer [are deftest]]
(:require [cljs.test :refer [are deftest is]]
[logseq.graph-parser.property :as gp-property]))
(deftest test->new-properties
@ -24,3 +24,16 @@
"hello\n:PROPERTIES:\n:foo: bar\n:nice\n:END:\nnice"
"hello\nfoo:: bar\n:nice\nnice"))
(deftest property-value-from-content
(is (= "62b38254-4be7-4627-a2b7-6d9ee20999e5"
(gp-property/property-value-from-content
"id"
"type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
"Pulls value from end of block content")
(is (= "nice walkthrough on creating a blog with #nbb"
(gp-property/property-value-from-content
"desc"
"type:: blog-posting\ndesc:: nice walkthrough on creating a blog with #nbb\nid:: 62b38254-4be7-4627-a2b7-6d9ee20999e5"))
"Pulls value from middle of block content"))

View File

@ -1,5 +1,5 @@
(ns logseq.graph-parser.text-test
(:require [cljs.test :refer [are deftest testing]]
(:require [cljs.test :refer [are deftest testing is]]
[logseq.graph-parser.text :as text]))
(deftest test-get-page-name
@ -109,4 +109,9 @@
:tags "\"[[foo]], [[bar]]\"" "\"[[foo]], [[bar]]\""
:tags "baz, \"[[foo]], [[bar]]\"" #{"baz"})))
(deftest extract-page-refs-and-tags
(is (= #{"cljs" "nbb" "js" "amazing"}
(text/extract-page-refs-and-tags "This project is written with #cljs, #nbb and #js. #amazing!"))
"Don't extract punctation at end of a tag"))
#_(cljs.test/test-ns 'logseq.graph-parser.text-test)

View File

@ -1,9 +1,10 @@
(ns logseq.graph-parser-test
(:require [cljs.test :refer [deftest testing is]]
(:require [cljs.test :refer [deftest testing is are]]
[clojure.string :as string]
[logseq.graph-parser :as graph-parser]
[logseq.db :as ldb]
[logseq.graph-parser.block :as gp-block]
[logseq.graph-parser.property :as gp-property]
[datascript.core :as d]))
(deftest parse-file
@ -68,3 +69,139 @@
(test-property-order 4))
(testing "Sort order and persistence of 10 properties"
(test-property-order 10)))
(defn- quoted-property-values-test
[user-config]
(let [conn (ldb/start-conn)
_ (graph-parser/parse-file conn
"foo.md"
"- desc:: \"#foo is not a ref\""
{:extract-options {:user-config user-config}})
block (->> (d/q '[:find (pull ?b [* {:block/refs [*]}])
:in $
:where [?b :block/properties]]
@conn)
(map first)
first)]
(is (= {:desc "\"#foo is not a ref\""}
(:block/properties block))
"Quoted value is unparsed")
(is (= ["desc"]
(map :block/original-name (:block/refs block)))
"No refs from property value")))
(deftest quoted-property-values
(testing "With default config"
(quoted-property-values-test {}))
(testing "With :rich-property-values config"
(quoted-property-values-test {:rich-property-values? true})))
(deftest page-properties-persistence
(testing "Non-string property values"
(let [conn (ldb/start-conn)]
(graph-parser/parse-file conn
"lythe-of-heaven.md"
"rating:: 8\nrecommend:: true\narchive:: false"
{})
(is (= {:rating 8 :recommend true :archive false}
(->> (d/q '[:find (pull ?b [*])
:in $
:where [?b :block/properties]]
@conn)
(map (comp :block/properties first))
first)))))
(testing "Linkable built-in properties"
(let [conn (ldb/start-conn)
_ (graph-parser/parse-file conn
"lol.md"
"alias:: 233\ntags:: fun, facts"
{})
block (->> (d/q '[:find (pull ?b [:block/properties {:block/alias [:block/name]} {:block/tags [:block/name]}])
:in $
:where [?b :block/name "lol"]]
@conn)
(map first)
first)]
(is (= {:block/alias [{:block/name "233"}]
:block/tags [{:block/name "fun"} {:block/name "facts"}]
:block/properties {:alias ["233"] :tags ["fun" "facts"]}}
block))
(is (every? vector? (vals (:block/properties block)))
"Linked built-in property values as vectors provides for easier transforms"))))
(defn- property-relationships-test
"Runs tests on page properties and block properties. file-properties is what is
visible in a file and db-properties is what is pulled out from the db"
[file-properties db-properties user-config]
(let [conn (ldb/start-conn)
page-content (gp-property/->block-content file-properties)
;; Create Block properties from given page ones
block-property-transform (fn [m] (update-keys m #(keyword (str "block-" (name %)))))
block-content (gp-property/->block-content (block-property-transform file-properties))
_ (graph-parser/parse-file conn
"property-relationships.md"
(str page-content "\n- " block-content)
{:extract-options {:user-config user-config}})
pages (->> (d/q '[:find (pull ?b [* :block/properties])
:in $
:where [?b :block/name] [?b :block/properties]]
@conn)
(map first))
_ (assert (= 1 (count pages)))
blocks (->> (d/q '[:find (pull ?b [:block/pre-block? :block/properties
{:block/refs [:block/original-name]}])
:in $
:where [?b :block/properties] [(missing? $ ?b :block/name)]]
@conn)
(map first)
(map (fn [m] (update m :block/refs #(map :block/original-name %)))))
block-db-properties (block-property-transform db-properties)]
(is (= db-properties (:block/properties (first pages)))
"page has expected properties")
(is (= [true nil] (map :block/pre-block? blocks))
"page has 2 blocks, one of which is a pre-block")
(is (= [db-properties block-db-properties]
(map :block/properties blocks))
"pre-block/page and block have expected properties")
;; has expected refs
(are [db-props refs]
(= (->> (vals db-props)
;; ignore string values
(mapcat #(if (coll? %) % []))
(concat (map name (keys db-props)))
set)
(set refs))
; pre-block/page has expected refs
db-properties (first (map :block/refs blocks))
;; block has expected refs
block-db-properties (second (map :block/refs blocks)))))
(deftest property-relationships
(let [properties {:single-link "[[bar]]"
:multi-link "[[Logseq]] is the fastest #triples #[[text editor]]"
:desc "This is a multiple sentence description. It has one [[link]]"
:comma-prop "one, two,three"}]
(testing "With default config"
(property-relationships-test
properties
{:single-link #{"bar"}
:multi-link #{"Logseq" "is the fastest" "triples" "text editor"}
:desc #{"This is a multiple sentence description. It has one" "link"}
:comma-prop #{"one" "two" "three"}}
{}))
(testing "With :rich-property-values config"
(property-relationships-test
properties
{:single-link #{"bar"}
:multi-link #{"Logseq" "triples" "text editor"}
:desc #{"link"}
:comma-prop "one, two,three"}
{:rich-property-values? true}))))

View File

@ -66,7 +66,7 @@
:devtools {:enabled false}
;; disable :static-fns to allow for with-redefs and repl development
:compiler-options {:static-fns false}
:main frontend.test.node-test-runner/main}
:main frontend.test.frontend-node-test-runner/main}
:publishing {:target :browser
:module-loader true

View File

@ -63,6 +63,7 @@
[logseq.graph-parser.config :as gp-config]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.text :as text]
[logseq.graph-parser.property :as gp-property]
[logseq.graph-parser.util :as gp-util]
[logseq.graph-parser.util.page-ref :as page-ref]
[logseq.graph-parser.util.block-ref :as block-ref]
@ -1803,9 +1804,15 @@
[:span ", "])
(rum/defc property-cp
[config block k v]
(let [date (and (= k :date) (date/get-locale-string (str v)))
property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? (state/get-config)))]
[config block k value]
(let [date (and (= k :date) (date/get-locale-string (str value)))
user-config (state/get-config)
;; In this mode and when value is a set of refs, display full property text
;; because :block/properties value only contains refs but user wants to see text
v (if (and (:rich-property-values? user-config) (coll? value))
(gp-property/property-value-from-content (name k) (:block/content block))
value)
property-pages-enabled? (contains? #{true nil} (:property-pages/enabled? user-config))]
[:div
(if property-pages-enabled?
(page-cp (assoc config :property? true) {:block/name (subs (str k) 1)})

View File

@ -6,7 +6,6 @@
[clojure.set :as set]
[clojure.string :as string]
[clojure.walk :as walk]
[frontend.state :as state]
[frontend.date :as date]
[frontend.db.model :as model]
[frontend.db.query-react :as query-react]
@ -235,12 +234,20 @@
(= 4 (count e))
(build-between-three-arg e)))
(defn parse-property-value
"Parses non-string property values or any page-ref like values"
[v]
(if-some [res (text/parse-non-string-property-value v)]
res
(text/split-page-refs-without-brackets v)))
(defn- build-property-two-arg
[e]
(let [k (string/replace (name (nth e 1)) "_" "-")
v (nth e 2)
v (if-not (nil? v)
(text/parse-property k v (state/get-config))
(parse-property-value (str v))
v)
v (if (coll? v) (first v) v)]
{:query (list 'property '?b (keyword k) v)
@ -285,7 +292,7 @@
(let [[k v] (rest e)
k (string/replace (name k) "_" "-")]
(if (some? v)
(let [v' (text/parse-property k v (state/get-config))
(let [v' (parse-property-value (str v))
val (if (coll? v') (first v') v')]
{:query (list 'page-property '?p (keyword k) val)
:rules [:page-property]})

View File

@ -3,7 +3,7 @@
[clojure.string :as str]
[frontend.db :as db]
[frontend.db.query-dsl :as query-dsl]
[frontend.test.helper :as test-helper :refer [load-test-files]]))
[frontend.test.helper :as test-helper :include-macros true :refer [load-test-files]]))
;; TODO: quickcheck
;; 1. generate query filters
@ -45,7 +45,8 @@
;; Tests
;; =====
(deftest block-property-queries
(defn- block-property-queries-test
[]
(load-test-files [{:file/path "journals/2022_02_28.md"
:file/content "a:: b
- b1
@ -112,15 +113,27 @@ prop-d:: nada"}])
(dsl-query "(property prop-d)")))
"Blocks that have a property"))
(deftest page-property-queries
(deftest block-property-queries
(testing "block property tests with default config"
(test-helper/with-config {}
(block-property-queries-test)))
(test-helper/start-test-db!) ;; reset db
(testing "block property tests with rich-property-values? config"
(test-helper/with-config {:rich-property-values? true}
(block-property-queries-test))))
(defn- page-property-queries-test
[]
(load-test-files [{:file/path "pages/page1.md"
:file/content "parent:: [[child page 1]], [[child-no-space]]"}
:file/content "parent:: [[child page 1]], [[child-no-space]]\ninteresting:: true"}
{:file/path "pages/page2.md"
:file/content "foo:: bar"}
:file/content "foo:: #bar\ninteresting:: false"}
{:file/path "pages/page3.md"
:file/content "parent:: [[child page 1]], child page 2\nfoo:: bar"}
:file/content "parent:: [[child page 1]], [[child page 2]]\nfoo:: bar\ninteresting:: false"}
{:file/path "pages/page4.md"
:file/content "parent:: child page 2\nfoo:: baz"}])
:file/content "parent:: [[child page 2]]\nfoo:: baz"}])
(is (= ["page1" "page3" "page4"]
(map :block/name (dsl-query "(page-property parent)")))
@ -160,7 +173,27 @@ prop-d:: nada"}])
(map
:block/name
(dsl-query "(and (not (page-property foo bar)) (page-property parent [[child page 2]]))")))
"Page property queries nested NOT in first clause"))
"Page property queries nested NOT in first clause")
(testing "boolean values"
(is (= ["page1"]
(map :block/name (dsl-query "(page-property interesting true)")))
"Boolean true")
(is (= ["page2" "page3"]
(map :block/name (dsl-query "(page-property interesting false)")))
"Boolean false")))
(deftest page-property-queries
(testing "page property tests with default config"
(test-helper/with-config {}
(page-property-queries-test)))
(test-helper/start-test-db!) ;; reset db
(testing "page property tests with rich-property-values? config"
(test-helper/with-config {:rich-property-values? true}
(page-property-queries-test))))
(deftest task-queries
(load-test-files [{:file/path "pages/page1.md"

View File

@ -0,0 +1,19 @@
(ns frontend.test.frontend-node-test-runner
"This is a custom version of the node-test-runner for the frontend build"
{:dev/always true} ;; necessary for test-data freshness
(:require [frontend.test.node-test-runner :as node-test-runner]
[shadow.test.env :as env]
[lambdaisland.glogi.console :as glogi-console]
;; activate humane test output for all tests
[pjstadig.humane-test-output]))
;; Needed for new test runners
(defn ^:dev/after-load reset-test-data! []
(-> (env/get-test-data)
(env/reset-test-data!)))
(defn main [& args]
[]
(glogi-console/install!) ;; see log messages
(reset-test-data!)
(node-test-runner/parse-and-run-tests args))

View File

@ -0,0 +1,8 @@
(ns frontend.test.helper)
(defmacro with-config
[config & body]
`(let [repo# (frontend.state/get-current-repo)]
(frontend.state/set-config! repo# ~config)
~@body
(frontend.state/set-config! repo# nil)))

View File

@ -1,6 +1,6 @@
(ns frontend.test.node-test-runner
"shadow-cljs test runner for :node-test that provides the same test selection
options as
"Application agnostic shadow-cljs test runner for :node-test that provides the
same test selection options as
https://github.com/cognitect-labs/test-runner#invoke-with-clojure--m-clojuremain.
This gives the user a fair amount of control over which tests and namespaces
to call from the commandline. Once this test runner is stable enough we should
@ -12,9 +12,7 @@
[clojure.set :as set]
[shadow.test :as st]
[cljs.test :as ct]
["util" :as util]
;; activate humane test output for all tests
[pjstadig.humane-test-output]))
[goog.string :as gstring]))
;; Cljs.test customization
;; Inherit behavior from default reporter
@ -47,7 +45,7 @@
(defn- print-summary
"Print help summary given args and opts strings"
[options-summary additional-msg]
(println (util/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
(println (gstring/format "Usage: %s [OPTIONS]\nOptions:\n%s%s"
"$0"
options-summary
additional-msg)))
@ -172,9 +170,9 @@ returns selected tests and namespaces to run"
(st/run-test-vars test-env test-vars))
(st/run-all-tests test-env nil))))
(defn main [& args]
(reset-test-data!)
(defn parse-and-run-tests
"Main entry point for custom test runners"
[args]
(let [{:keys [options summary]} (parse-options args cli-options)]
(if (:help options)
(do
@ -182,3 +180,9 @@ returns selected tests and namespaces to run"
"\n\nMultiple options are ANDed. Defaults to running all tests")
(js/process.exit 0))
(run-tests (keys (env/get-tests)) (env/get-test-vars) options))))
(defn main
"Main entry point if this ns is configured as a test runner"
[& args]
(reset-test-data!)
(parse-and-run-tests args))

View File

@ -228,6 +228,10 @@
;; E.g.:property-pages/excludelist #{:duration :author}
;; :property-pages/excludelist
;; Enables property values to contain a mix of tags, page-refs, special
;; punctuation and free-form text
;; :rich-property-values? true
;; logbook setup
;; :logbook/settings
;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated