Skip to content
Snippets Groups Projects
Unverified Commit 99ad6c05 authored by Noah Moss's avatar Noah Moss Committed by GitHub
Browse files

Optional blocks in Markdown cards (#24491)

parent a2de3c7c
No related branches found
No related tags found
No related merge requests found
(ns metabase.shared.parameters.parameters
"Util functions for dealing with parameters"
"Util functions for dealing with parameters. Primarily used for substituting parameters into variables in Markdown
dashboard cards."
#?@
(:clj
[(:require [clojure.string :as str]
......@@ -113,36 +114,92 @@
[text]
(str/replace text escaped-chars-regex #(str \\ %)))
(defn- replacement
[tag->param locale match]
(let [tag-name (second match)
param (get tag->param tag-name)
(defn- value
[tag-name tag->param locale]
(let [param (get tag->param tag-name)
value (:value param)
tyype (:type param)]
(if value
(when value
(try (-> (formatted-value tyype value locale)
escape-chars)
(catch #?(:clj Throwable :cljs js/Error) _
;; If we got an exception (most likely during date parsing/formatting), fallback to the default
;; implementation of formatted-value
(formatted-value :default value locale)))
;; If this parameter has no value, return the original {{tag}} so that no substitution is done.
(first match))))
(defn- normalize-parameter
"Normalize a single parameter by calling [[mbql.normalize/normalize-fragment]] on it, and converting all string keys
to keywords."
[parameter]
(->> (mbql.normalize/normalize-fragment [:parameters] [parameter])
first
(reduce-kv (fn [acc k v] (assoc acc (keyword k) v)) {})))
(formatted-value :default value locale))))))
(def ^:private template-tag-regex
"A regex to find template tags in a text card on a dashboard. This should mirror the regex used to find template
tags in native queries, with the exception of snippets and card ID references (see the metabase-lib function
`recognizeTemplateTags` for that regex)."
`recognizeTemplateTags` for that regex).
If you modify this, also modify `template-tag-splitting-regex` below."
#"\{\{\s*([A-Za-z0-9_\.]+?)\s*\}\}")
;; Represents a variable parsed out of a text card. `tag` contains the tag name alone, as a string. `source` contains
;; the full original syntax for the parameter)
(defrecord ^:private TextParam [tag source]
Object
(toString
[x]
(or (:value x) source)))
(defn- TextParam?
[x]
(instance? TextParam x))
(def ^:private template-tag-splitting-regex
(let [base "\\{\\{\\s*[A-Za-z0-9_\\.]+?\\s*\\}\\}"]
;; Use lookahead and lookbehind to retain matches in split
(re-pattern (str "(?<=" base ")|(?=" base ")"))))
(defn- split-on-tags
"Given the text of a Markdown card, splits it into a sequence of alternating strings and TextParam records."
[text]
(let [split-text (str/split text template-tag-splitting-regex)]
(map (fn [text]
(if-let [[_, match] (re-matches template-tag-regex text)]
(->TextParam match text)
text))
split-text)))
(defn- join-consecutive-strings
"Given a vector of strings and/or TextParam, concatenate consecutive strings and TextParams without values."
[strs-or-vars]
(->> strs-or-vars
(partition-by (fn [str-or-var]
(or (string? str-or-var)
(not (:value str-or-var)))))
(mapcat (fn [strs-or-var]
(if (string? (first strs-or-var))
[(str/join strs-or-var)]
strs-or-var)))))
(defn- add-values-to-variables
"Given `split-text`, containing a list of alternating strings and TextParam, add a :value key to any TextParams
with a corresponding value in `tag->normalized-param`."
[tag->normalized-param locale split-text]
(map
(fn [maybe-variable]
(if (TextParam? maybe-variable)
(assoc maybe-variable :value (value (:tag maybe-variable) tag->normalized-param locale))
maybe-variable))
split-text))
(def ^:private optional-block-regex
#"\[\[.+\]\]")
(def ^:private non-optional-block-regex
#"\[\[(.+?)\]\]")
(defn- strip-optional-blocks
"Removes any [[optional]] blocks from individual strings in `split-text`, which are blocks that have no parameters
with values. Then, concatenates the full string and removes the brackets from any remaining optional blocks."
[split-text]
(let [s (->> split-text
(map #(if (TextParam? %) % (str/replace % optional-block-regex "")))
str/join)]
(str/replace s non-optional-block-regex second)))
(defn ^:export tag_names
"Given the content of a text dashboard card, return a set of the unique names of template tags in the text."
[text]
......@@ -152,6 +209,14 @@
#?(:clj tag-names
:cljs (clj->js tag-names))))
(defn- normalize-parameter
"Normalize a single parameter by calling [[mbql.normalize/normalize-fragment]] on it, and converting all string keys
to keywords."
[parameter]
(->> (mbql.normalize/normalize-fragment [:parameters] [parameter])
first
(reduce-kv (fn [acc k v] (assoc acc (keyword k) v)) {})))
(defn ^:export substitute_tags
"Given the context of a text dashboard card, replace all template tags in the text with their corresponding values,
formatted and escaped appropriately."
......@@ -165,4 +230,17 @@
(assoc acc tag (normalize-parameter param)))
{}
tag->param)]
(str/replace text template-tag-regex (partial replacement tag->normalized-param locale))))))
;; Most of the functions in this pipeline are relating to handling optional blocks in the text which use
;; the [[ ]] syntax.
;; For example, given an input "[[a {{b}}]] [[{{c}}]]", where `b` has no value and `c` = 3:
;; 1. `split-on-tags` =>
;; ("[[a " {:tag "b" :source "{{b}}"} "]] [[" {:tag "c" :source "{{c}}"} "]]")
;; 2. `add-values-to-variables` =>
;; ("[[a " {:tag "b" :source "{{b}}" :value nil} "]] [[" {:tag "c" :source "{{c}}" :value 3} "]]")
;; 3. `join-consecutive-strings` => ("[[a {{b}}]] [[" {:tag "b" :source "{{c}}" :value 3} "]])
;; 4. `strip-optional-blocks` => "3"
(->> text
split-on-tags
(add-values-to-variables tag->normalized-param locale)
join-consecutive-strings
strip-optional-blocks)))))
......@@ -185,3 +185,49 @@
"{{foo}}"
{"foo" {:type :date/month-year :value "2019-08"}}
"agosto\\, 2019"))))
(t/deftest substitute-tags-optional-blocks-test
(t/testing "Optional blocks are removed when necessary"
(t/are [text tag->param expected] (= expected (params/substitute_tags text tag->param))
"[[{{foo}}]]"
{}
""
"[[{{foo}}]]"
{"foo" {:type :string/= :value "bar"}}
"bar"
"Customers[[ with over {{order_count}} orders]]"
{"order_count" {:type :number/= :value nil}}
"Customers"
"Customers[[ with over {{order_count}} orders]]"
{"order_count" {:type :number/= :value 10}}
"Customers with over 10 orders"
;; Optional block is retained when *any* parameters within are substituted
"[[{{foo}} {{baz}}]]"
{"foo" {:type :string/= :value "bar"}}
"bar {{baz}}"
;; Make sure `join-consecutive-strings` retains consecutive non-strings (this was a bug during implementation)
"[[{{foo}}{{foo}}]]"
{"foo" {:type :string/= :value "foo"}}
"foofoo"
"[[{{foo}}]] [[{{bar}}]]"
{"foo" {:type :string/= :value 1} "bar" {:type :string/= :value 2}}
"1 2"
"[[{{foo}}]"
{"foo" {:type :string/= :value "bar"}}
"[[bar]"
"[{{foo}}]]"
{"foo" {:type :string/= :value "bar"}}
"[bar]]"
;; Don't strip square brackets that are in parameter values
"{{foo}}"
{"foo" {:type :string/= :value "[[bar]]"}}
"\\[\\[bar\\]\\]")))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment