Merge branch 'master' into enhance/property-ux

This commit is contained in:
Tienson Qin
2026-04-24 19:15:01 +08:00
9 changed files with 452 additions and 172 deletions

View File

@@ -29,6 +29,8 @@ i18n_functions = [
"tt",
"i18n/t",
"i18n/tt",
# Parameterized translation function.
"t-fn",
]
# Alert/notification functions.

View File

@@ -169,6 +169,7 @@
"react-transition-group": "4.4.5",
"react-virtuoso": "4.18.3",
"remove-accents": "0.5.0",
"tiny-pinyin": "1.3.2",
"sanitize-filename": "1.6.4",
"send-intent": "^7.0.0",
"url": "^0.11.4",

View File

@@ -1,7 +1,7 @@
(ns frontend.commands
"Provides functionality for commands and advanced commands"
(:require [clojure.string :as string]
[frontend.context.i18n :refer [interpolate-sentence t]]
[frontend.context.i18n :as i18n :refer [interpolate-sentence t t-en]]
[frontend.date :as date]
[frontend.db :as db]
[frontend.extensions.video.youtube :as youtube]
@@ -148,42 +148,44 @@
[:editor/set-property :logseq.property.node/display-type :math]])
(defn get-statuses
[]
(let [group-label (t :editor.slash/group-task-status)
result (->>
(db-based-statuses)
(mapv (fn [status]
(let [command (:block/title status)
label (db-property/built-in-display-title status t)
icon (case command
"Canceled" "Cancelled"
"Doing" "InProgress50"
command)]
[label (->marker command) (t :editor.slash/status-desc label) icon]))))]
(when (seq result)
(map (fn [v] (conj v group-label)) result))))
([] (get-statuses t))
([t-fn]
(let [group-label (t-fn :editor.slash/group-task-status)
result (->>
(db-based-statuses)
(mapv (fn [status]
(let [command (:block/title status)
label (db-property/built-in-display-title status t-fn)
icon (case command
"Canceled" "Cancelled"
"Doing" "InProgress50"
command)]
[label (->marker command) (t-fn :editor.slash/status-desc label) icon]))))]
(when (seq result)
(map (fn [v] (conj v group-label)) result)))))
(defn db-based-priorities
[]
(db-pu/get-closed-property-values :logseq.property/priority))
(defn get-priorities
[]
(let [group-label (t :editor.slash/group-priority)
with-no-priority #(cons [(t :editor.slash/no-priority) (->priority nil) "" :icon/priorityLvlNone] %)
result (->>
(db-based-priorities)
(mapv (fn [priority]
(let [value (:block/title priority)
label (db-property/built-in-display-title priority t)]
[(t :editor.slash/priority-label label)
(->priority value)
(t :editor.slash/priority-desc label)
(str "priorityLvl" value)])))
(with-no-priority)
(vec))]
(when (seq result)
(map (fn [v] (into v [group-label])) result))))
([] (get-priorities t))
([t-fn]
(let [group-label (t-fn :editor.slash/group-priority)
with-no-priority #(cons [(t-fn :editor.slash/no-priority) (->priority nil) "" :icon/priorityLvlNone] %)
result (->>
(db-based-priorities)
(mapv (fn [priority]
(let [value (:block/title priority)
label (db-property/built-in-display-title priority t-fn)]
[(t-fn :editor.slash/priority-label label)
(->priority value)
(t-fn :editor.slash/priority-desc label)
(str "priorityLvl" value)])))
(with-no-priority)
(vec))]
(when (seq result)
(map (fn [v] (into v [group-label])) result)))))
;; Credits to roamresearch.com
@@ -194,158 +196,188 @@
[:editor/move-cursor-to-end]])
(defn- headings
[]
(into [[(t :editor.slash/normal-text)
(->heading nil)
(t :editor.slash/normal-text-desc)
:icon/text
(t :editor.slash/group-heading)]]
(mapv (fn [level]
(let [heading (t :editor.slash/heading-label level)]
[heading (->heading level) heading (str "h-" level) (t :editor.slash/group-heading)]))
(range 1 7))))
([] (headings t))
([t-fn]
(into [[(t-fn :editor.slash/normal-text)
(->heading nil)
(t-fn :editor.slash/normal-text-desc)
:icon/text
(t-fn :editor.slash/group-heading)]]
(mapv (fn [level]
(let [heading (t-fn :editor.slash/heading-label level)]
[heading (->heading level) heading (str "h-" level) (t-fn :editor.slash/group-heading)]))
(range 1 7)))))
(defonce *latest-matched-command (atom ""))
(defonce *matched-commands (atom nil))
(defonce *initial-commands (atom nil))
(defn ^:large-vars/cleanup-todo commands-map
[get-page-ref-text]
(let [embed-block db-based-embed-block]
(->>
(concat
;; basic
[[(t :editor.slash/node-reference)
[[:editor/input page-ref/left-and-right-brackets {:backward-pos 2}]
[:editor/search-page]]
(t :editor.slash/node-reference-desc)
:icon/pageRef
(t :editor.slash/group-basic)]
[(t :editor.slash/node-embed)
(embed-block)
(t :editor.slash/node-embed-desc)
:icon/blockEmbed]]
([get-page-ref-text] (commands-map get-page-ref-text t))
([get-page-ref-text t-fn]
(let [embed-block db-based-embed-block]
(->>
(concat
;; basic
[[(t-fn :editor.slash/node-reference)
[[:editor/input page-ref/left-and-right-brackets {:backward-pos 2}]
[:editor/search-page]]
(t-fn :editor.slash/node-reference-desc)
:icon/pageRef
(t-fn :editor.slash/group-basic)]
[(t-fn :editor.slash/node-embed)
(embed-block)
(t-fn :editor.slash/node-embed-desc)
:icon/blockEmbed]]
;; format
[[(t :ui/link) (link-steps) (t :editor.slash/link-desc) :icon/link (t :editor.slash/group-format)]
[(t :editor.slash/image-link) (image-link-steps) (t :editor.slash/image-link-desc) :icon/photoLink]
(when (state/markdown?)
[(t :editor.slash/underline) [[:editor/input "<ins></ins>"
{:last-pattern command-trigger
:backward-pos 6}]] (t :editor.slash/underline-desc)
:icon/underline])
[(t :editor.slash/code-block)
(code-block-steps)
(t :editor.slash/code-block-desc)
:icon/code]
[(t :class.built-in/quote-block)
(quote-block-steps)
(t :editor.slash/quote-desc)
:icon/quote]
[(t :editor.slash/math-block)
(math-block-steps)
(t :editor.slash/math-block-desc)
:icon/math]]
;; format
[[(t-fn :ui/link) (link-steps) (t-fn :editor.slash/link-desc) :icon/link (t-fn :editor.slash/group-format)]
[(t-fn :editor.slash/image-link) (image-link-steps) (t-fn :editor.slash/image-link-desc) :icon/photoLink]
(when (state/markdown?)
[(t-fn :editor.slash/underline)
[[:editor/input "<ins></ins>" {:last-pattern command-trigger :backward-pos 6}]]
(t-fn :editor.slash/underline-desc)
:icon/underline])
[(t-fn :editor.slash/code-block)
(code-block-steps)
(t-fn :editor.slash/code-block-desc)
:icon/code]
[(t-fn :class.built-in/quote-block)
(quote-block-steps)
(t-fn :editor.slash/quote-desc)
:icon/quote]
[(t-fn :editor.slash/math-block)
(math-block-steps)
(t-fn :editor.slash/math-block-desc)
:icon/math]]
(headings)
(headings t-fn)
;; task management
(get-statuses)
;; task management
(get-statuses t-fn)
;; task date
[[(t :property.built-in/deadline)
[[:editor/clear-current-slash]
[:editor/set-deadline]]
""
:icon/calendar-stats
(t :editor.slash/group-task-date)]
[(t :property.built-in/scheduled)
[[:editor/clear-current-slash]
[:editor/set-scheduled]]
""
:icon/calendar-month
(t :editor.slash/group-task-date)]]
;; task date
[[(t-fn :property.built-in/deadline)
[[:editor/clear-current-slash]
[:editor/set-deadline]]
""
:icon/calendar-stats
(t-fn :editor.slash/group-task-date)]
[(t-fn :property.built-in/scheduled)
[[:editor/clear-current-slash]
[:editor/set-scheduled]]
""
:icon/calendar-month
(t-fn :editor.slash/group-task-date)]]
;; priority
(get-priorities)
;; priority
(get-priorities t-fn)
;; time & date
[[(t :date.nlp/tomorrow)
#(get-page-ref-text (db/get-journal-page-title (date/tomorrow)))
(t :editor.slash/tomorrow-desc)
:icon/tomorrow
(t :editor.slash/group-time-and-date)]
[(t :date.nlp/yesterday) #(get-page-ref-text (db/get-journal-page-title (date/yesterday))) (t :editor.slash/yesterday-desc) :icon/yesterday]
[(t :date.nlp/today) #(get-page-ref-text (db/get-today-journal-title)) (t :editor.slash/today-desc) :icon/calendar]
[(t :editor.slash/current-time) #(date/get-current-time) (t :editor.slash/current-time-desc) :icon/clock]
[(t :editor.slash/date-picker) [[:editor/show-date-picker]] (t :editor.slash/date-picker-desc) :icon/calendar-dots]]
;; time & date
[[(t-fn :date.nlp/tomorrow)
#(get-page-ref-text (db/get-journal-page-title (date/tomorrow)))
(t-fn :editor.slash/tomorrow-desc)
:icon/tomorrow
(t-fn :editor.slash/group-time-and-date)]
[(t-fn :date.nlp/yesterday)
#(get-page-ref-text (db/get-journal-page-title (date/yesterday)))
(t-fn :editor.slash/yesterday-desc)
:icon/yesterday]
[(t-fn :date.nlp/today)
#(get-page-ref-text (db/get-today-journal-title))
(t-fn :editor.slash/today-desc)
:icon/calendar]
[(t-fn :editor.slash/current-time)
#(date/get-current-time)
(t-fn :editor.slash/current-time-desc)
:icon/clock]
[(t-fn :editor.slash/date-picker)
[[:editor/show-date-picker]]
(t-fn :editor.slash/date-picker-desc)
:icon/calendar-dots]]
;; order list
[[(t :editor.slash/number-list)
[[:editor/clear-current-slash]
[:editor/toggle-own-number-list]]
(t :editor.slash/number-list)
:icon/numberedParents
(t :editor.slash/group-list-type)]
[(t :editor.slash/number-children) [[:editor/clear-current-slash]
[:editor/toggle-children-number-list]]
(t :editor.slash/number-children)
:icon/numberedChildren]]
;; order list
[[(t-fn :editor.slash/number-list)
[[:editor/clear-current-slash]
[:editor/toggle-own-number-list]]
(t-fn :editor.slash/number-list)
:icon/numberedParents
(t-fn :editor.slash/group-list-type)]
[(t-fn :editor.slash/number-children)
[[:editor/clear-current-slash]
[:editor/toggle-children-number-list]]
(t-fn :editor.slash/number-children)
:icon/numberedChildren]]
;; advanced
[[(t :property.built-in/query) (query-steps) (query-doc) :icon/query (t :editor.slash/group-advanced)]
[(t :editor.slash/advanced-query) (advanced-query-steps) (t :editor.slash/advanced-query-desc) :icon/query]
[(t :editor.slash/query-function) [[:editor/input "{{function }}" {:backward-pos 2}]] (t :editor.slash/query-function-desc) :icon/queryCode]
[(t :editor.slash/calculator)
(calc-steps)
(t :editor.slash/calculator-desc) :icon/calculator]
;; advanced
[[(t-fn :property.built-in/query) (query-steps) (query-doc) :icon/query (t-fn :editor.slash/group-advanced)]
[(t-fn :editor.slash/advanced-query) (advanced-query-steps) (t-fn :editor.slash/advanced-query-desc) :icon/query]
[(t-fn :editor.slash/query-function)
[[:editor/input "{{function }}" {:backward-pos 2}]]
(t-fn :editor.slash/query-function-desc)
:icon/queryCode]
[(t-fn :editor.slash/calculator)
(calc-steps)
(t-fn :editor.slash/calculator-desc)
:icon/calculator]
[(t-fn :editor.slash/upload-asset)
[[:editor/click-hidden-file-input :id]]
(t-fn :editor.slash/upload-asset-desc)
:icon/upload]
[(t-fn :class.built-in/template)
[[:editor/input command-trigger nil]
[:editor/search-template]]
(t-fn :editor.slash/template-desc)
:icon/template]
[(t-fn :editor.slash/embed-html) (->inline "html") "" :icon/htmlEmbed]
[(t-fn :editor.slash/embed-video-url)
[[:editor/input "{{video }}" {:last-pattern command-trigger :backward-pos 2}]]
""
:icon/videoEmbed]
[(t-fn :editor.slash/embed-youtube-timestamp) [[:youtube/insert-timestamp]] "" :icon/videoEmbed]
[(t-fn :editor.slash/embed-twitter-tweet)
[[:editor/input "{{tweet }}" {:last-pattern command-trigger :backward-pos 2}]]
""
:icon/xEmbed]
[(t-fn :command.editor/add-property)
[[:editor/clear-current-slash]
[:editor/new-property]]
""
:icon/cube-plus]]
[(t :editor.slash/upload-asset)
[[:editor/click-hidden-file-input :id]]
(t :editor.slash/upload-asset-desc)
:icon/upload]
[(t :class.built-in/template) [[:editor/input command-trigger nil]
[:editor/search-template]] (t :editor.slash/template-desc)
:icon/template]
[(t :editor.slash/embed-html) (->inline "html") "" :icon/htmlEmbed]
[(t :editor.slash/embed-video-url) [[:editor/input "{{video }}" {:last-pattern command-trigger
:backward-pos 2}]] ""
:icon/videoEmbed]
[(t :editor.slash/embed-youtube-timestamp) [[:youtube/insert-timestamp]] "" :icon/videoEmbed]
[(t :editor.slash/embed-twitter-tweet) [[:editor/input "{{tweet }}" {:last-pattern command-trigger
:backward-pos 2}]] ""
:icon/xEmbed]
[(t :command.editor/add-property) [[:editor/clear-current-slash]
[:editor/new-property]] ""
:icon/cube-plus]]
(let [commands (->> @*extend-slash-commands
(map resolve-slash-command)
(remove (fn [command] (when (map? (last command))
(false? (:db-graph? (last command)))))))]
commands)
(let [commands (->> @*extend-slash-commands
(map resolve-slash-command)
(remove (fn [command]
(when (map? (last command))
(false? (:db-graph? (last command)))))))]
commands)
;; Allow user to modify or extend, should specify how to extend.
(state/get-commands)
(when-let [plugin-commands (seq (some->> (state/get-plugins-slash-commands)
(mapv #(vec (concat % [nil :icon/puzzle])))))]
(-> plugin-commands (vec) (update 0 (fn [v] (conj v (t :editor.slash/group-plugins)))))))
(remove nil?)
(util/distinct-by-last-wins first))))
(state/get-commands)
(when-let [plugin-commands (seq (some->> (state/get-plugins-slash-commands)
(mapv #(vec (concat % [nil :icon/puzzle])))))]
(-> plugin-commands
(vec)
(update 0 (fn [v] (conj v (t-fn :editor.slash/group-plugins)))))))
(remove nil?)
(util/distinct-by-last-wins first)))))
(defn init-commands!
[get-page-ref-text]
(let [commands (commands-map get-page-ref-text)]
(let [commands (commands-map get-page-ref-text)
en-commands (commands-map get-page-ref-text t-en)
lang (or (some-> (:preferred-language @state/state) keyword) :en)
zh-cn? (= lang :zh-CN)
commands-with-meta
(mapv (fn [cmd en-cmd]
(let [m (cond-> {:en-text (first en-cmd)}
zh-cn? (assoc :pinyin-text (search/hanzi->initials (first cmd))))]
(with-meta cmd m)))
commands en-commands)]
(reset! *latest-matched-command "")
(reset! *initial-commands commands)
(reset! *matched-commands commands)))
(reset! *initial-commands commands-with-meta)
(reset! *matched-commands commands-with-meta)))
(defn set-matched-commands!
[command matched-commands]
@@ -513,9 +545,21 @@
([text]
(get-matched-commands text @*initial-commands))
([text commands]
(search/fuzzy-search commands text
:extract-fn first
:limit 50)))
(let [lang (or (some-> (:preferred-language @state/state) keyword) :en)
en? (= lang :en)
zh-cn? (= lang :zh-CN)
extract-fns (cond
en? [first]
zh-cn? [first
#(-> % meta :en-text)
#(-> % meta :pinyin-text)]
:else [first
#(-> % meta :en-text)])]
(search/fuzzy-search-multi
commands
text
{:extract-fns extract-fns
:limit 50}))))
(defmulti handle-step first)

View File

@@ -1,6 +1,7 @@
(ns frontend.common.search-fuzzy
"fuzzy search. Used by frontend and worker namespaces"
(:require ["remove-accents" :as removeAccents]
["tiny-pinyin" :as tp]
[cljs-bean.core :as bean]
[clojure.string :as string]))
@@ -88,3 +89,84 @@
{:data item
:score (score query s)})))))
(map :data)))
(defn fuzzy-search-multi
"Like fuzzy-search but scores each item against multiple extract-fns,
taking the best score. Extract-fns that return nil or blank strings are
skipped — no score is computed for those fields."
[data query & {:keys [limit extract-fns]
:or {limit 20}}]
(->> (take limit
(sort-by :score (comp - compare)
(filter #(< 0 (:score %))
(for [item data]
(let [strings (->> extract-fns
(map (fn [f] (str (f item))))
(remove string/blank?))
best-score (if (seq strings)
(apply max (map #(score query %) strings))
0)]
{:data item
:score best-score})))))
(map :data)))
(defn- zh-simplified-char?
"True for characters in the CJK Unified Ideographs blocks
used by Simplified Chinese."
[c]
(let [code (.charCodeAt c 0)]
(or (and (>= code 0x4E00) (<= code 0x9FFF))
(and (>= code 0x3400) (<= code 0x4DBF)))))
(defn- zh-initial
"Return the lowercase pinyin initial letter for a single Chinese character."
[c]
(string/lower-case (subs (tp/convertToPinyin c "" false) 0 1)))
(defn- segment->initials
"Strip punctuation and symbols from a non-Chinese text segment, split on
whitespace, and collect the first character of each resulting word.
Numbers are treated as words — '12345' contributes '1'; '1 23 45'
contributes '1', '2', '4'."
[seg]
(when (seq seg)
(->> (string/replace seg #"[^a-zA-Z0-9 ]" "")
(#(string/split % #"\s+"))
(remove string/blank?)
(map #(string/lower-case (subs % 0 1))))))
(defn hanzi->initials
"Derive a compact initial-letter string from mixed Chinese/English text for
Simplified-Chinese prefix search (pinyin-style). Processes Simplified
Chinese only — other CJK scripts are treated as non-Chinese segments.
Rules:
- Each Simplified Chinese character maps to the first letter of its pinyin.
- Non-Chinese text is stripped of punctuation, split on whitespace, and
contributes the first character of each resulting word.
- Numbers follow word rules: '12345' → '1'; '1 23 45' → '1', '2', '4'.
Examples:
'设置' → 'sz'
'删除块' → 'sck'
'新建页面' → 'xjym'
'插入 block embed' → 'crbe'
'导出为 PDF' → 'dcwp'
'今日日志' → 'jrrz'"
[s]
(when (string? s)
(loop [cs (seq s)
seg []
result []]
(cond
(empty? cs)
(string/join (into result (segment->initials (string/join seg))))
(zh-simplified-char? (str (first cs)))
(let [seg-initials (segment->initials (string/join seg))
init (zh-initial (str (first cs)))]
(recur (rest cs) [] (-> result (into seg-initials) (conj init))))
:else
(recur (rest cs) (conj seg (str (first cs))) result)))))

View File

@@ -7,7 +7,7 @@
[frontend.components.cmdk.state :as cmdk-state]
[frontend.components.icon :as icon-component]
[frontend.config :as config]
[frontend.context.i18n :as i18n :refer [t]]
[frontend.context.i18n :refer [interpolate-rich-text t t-en t-locale]]
[frontend.db :as db]
[frontend.db.async :as db-async]
[frontend.db.model :as model]
@@ -42,9 +42,10 @@
[]
(:action (:search/args @state/state)))
(defn translate [t {:keys [id desc]}]
(defn translate
[t-fn {:keys [id desc]}]
(when id
(let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
(let [desc-i18n (t-fn (shortcut-utils/decorate-namespace id))]
(if (string/starts-with? desc-i18n "{Missing key")
desc
desc-i18n))))
@@ -240,15 +241,55 @@
(reset! !results (assoc-in default-results [:recently-updated-pages :items] recent-pages)))))
;; The commands search uses the command-palette handler
(defn- translate-locale
"Return the locale-only translation for a command.
Returns nil when the locale is :en or the key has no translation in the
current locale — no English fallback is applied."
[{:keys [id]}]
(when id
(t-locale (shortcut-utils/decorate-namespace id))))
(defonce ^:private !commands-cache (atom {:lang nil :commands nil}))
(defn- get-commands-for-search
"Return commands with locale, English, and (for :zh-CN) pinyin-initial fields.
:locale-t — locale-only translation; nil when locale is :en or key has no
locale entry (no English fallback).
:en-t — English translation; always present.
:pinyin-t — Simplified Chinese pinyin initials; present only for :zh-CN.
Cached by language — rebuilt only when preferred-language changes."
[]
(let [lang (or (some-> (:preferred-language @state/state) keyword) :en)
cache @!commands-cache]
(if (= (:lang cache) lang)
(:commands cache)
(let [zh-cn? (= lang :zh-CN)
cmds (->> (cp-handler/top-commands 1000)
(map (fn [cmd]
(let [locale-t (when-not (= lang :en) (translate-locale cmd))
en-t (translate t-en cmd)]
(cond-> (assoc cmd :en-t en-t)
locale-t (assoc :locale-t locale-t)
(and zh-cn? locale-t) (assoc :pinyin-t (search/hanzi->initials locale-t)))))))]
(reset! !commands-cache {:lang lang :commands cmds})
cmds))))
(defmethod load-results :commands [group state]
(let [!input (::input state)
(let [!input (::input state)
!results (::results state)]
(swap! !results assoc-in [group :status] :loading)
(let [commands (->> (cp-handler/top-commands 1000)
(map #(assoc % :t (translate t %))))
(let [lang (or (some-> (:preferred-language @state/state) keyword) :en)
en? (= lang :en)
zh-cn? (= lang :zh-CN)
commands (get-commands-for-search)
extract-fns (cond
en? [:en-t]
zh-cn? [:locale-t :en-t :pinyin-t]
:else [:locale-t :en-t])
search-results (if (string/blank? @!input)
commands
(search/fuzzy-search commands @!input {:extract-fn :t}))]
(search/fuzzy-search-multi commands @!input
{:extract-fns extract-fns}))]
(->> search-results
(map #(hash-map :icon "command"
:icon-theme :gray
@@ -1111,7 +1152,7 @@
(defn- tip-with-shortcut
[template shortcut & [shortcut-opts]]
(into [:div.flex.flex-row.gap-1.items-center.opacity-50.hover:opacity-100]
(i18n/interpolate-rich-text
(interpolate-rich-text
template
[(shui/shortcut shortcut shortcut-opts)])))
@@ -1213,6 +1254,10 @@
(defn- cmdk-init-state
"Initialize cmdk component state atoms."
[state]
;; Invalidate the commands cache so that each new CMDK session gets a fresh
;; commands list from cp-handler/top-commands (plugins, graph state, etc. may
;; have changed since the last session).
(reset! !commands-cache {:lang nil :commands nil})
(let [raw-search-mode (:search/mode @state/state)
search-mode (or raw-search-mode :global)
search-args (:search/args @state/state)

View File

@@ -41,6 +41,31 @@
:zh-CN ""
:zh-Hant ""})
(def ^:private translate-strict
"tongue translator built against the raw locale dicts without any fallback.
Returns a '{Missing key ...}' string for keys absent in the requested locale."
(tongue/build-translate dicts/dicts))
(defn t-locale
"Translate using the user's current locale without English fallback for
missing keys — returns nil when the key has no translation in the current
locale. If translation throws (e.g. malformed value format), falls back to
English and logs the error; that is distinct from a missing-key nil return.
Callers that need a guaranteed string should use t."
[& args]
(let [lang (preferred-locale)
result (try
(apply translate-strict lang args)
(catch :default e
(log/error :failed-translation {:arguments args :lang lang})
(state/pub-event! [:capture-error {:error e
:payload {:type :failed-translation
:arguments args
:lang lang}}])
(apply translate :en args)))]
(when-not (string/starts-with? (str result) "{Missing key")
result)))
(defn t-en
"Translate using English locale, ignoring user preference.
Useful for user-facing text that also requires output to the console."

View File

@@ -12,6 +12,8 @@
[promesa.core :as p]))
(def fuzzy-search fuzzy/fuzzy-search)
(def fuzzy-search-multi fuzzy/fuzzy-search-multi)
(def hanzi->initials fuzzy/hanzi->initials)
(defn get-engine
[repo]

View File

@@ -0,0 +1,74 @@
(ns frontend.common.search-fuzzy-test
(:require [cljs.test :refer [deftest is testing]]
[frontend.common.search-fuzzy :as fuzzy]))
(deftest fuzzy-search-multi-finds-by-first-field
(testing "locale query still returns correct item when first field matches"
(let [data [{:locale "粗体" :en "Bold"}
{:locale "斜体" :en "Italic"}]]
(is (seq (fuzzy/fuzzy-search-multi data "粗体"
{:extract-fns [:locale :en]
:limit 5}))))))
(deftest fuzzy-search-multi-finds-by-second-field
(testing "returns item when query matches second field but not first"
(let [data [{:locale "粗体" :en "Bold"}
{:locale "斜体" :en "Italic"}
{:locale "代码" :en "Code"}]]
(is (= [{:locale "粗体" :en "Bold"}]
(fuzzy/fuzzy-search-multi data "bold"
{:extract-fns [:locale :en]
:limit 5}))))))
(deftest fuzzy-search-multi-score-prefers-best-field
(testing "item with exact match on any field ranks first"
(let [data [{:locale "粗体" :en "Bold"}
{:locale "粗" :en "Thick"}]
results (fuzzy/fuzzy-search-multi data "粗体"
{:extract-fns [:locale :en]
:limit 5})]
(is (= "粗体" (:locale (first results)))))))
(deftest fuzzy-search-multi-returns-empty-when-no-match
(testing "returns empty seq when query matches nothing"
(let [data [{:locale "粗体" :en "Bold"}]]
(is (empty? (fuzzy/fuzzy-search-multi data "xyz"
{:extract-fns [:locale :en]
:limit 5}))))))
(deftest fuzzy-search-multi-skips-nil-extract-fields
(testing "nil fields from extract-fns are silently skipped, non-nil fields still score"
(let [data [{:en "Delete Page" :locale nil}
{:en "New Page" :locale nil}]]
(is (= [{:en "Delete Page" :locale nil}]
(fuzzy/fuzzy-search-multi data "delete"
{:extract-fns [:locale :en]
:limit 5}))))))
(deftest hanzi->initials-all-chinese
(testing "pure Chinese command names produce correct pinyin initials"
(is (= "sck" (fuzzy/hanzi->initials "删除块")))
(is (= "xjym" (fuzzy/hanzi->initials "新建页面")))
(is (= "jrrz" (fuzzy/hanzi->initials "今日日志")))
(is (= "sz" (fuzzy/hanzi->initials "设置")))))
(deftest hanzi->initials-mixed-chinese-english
(testing "mixed Chinese and English text combines pinyin and word initials"
(is (= "crbe" (fuzzy/hanzi->initials "插入 block embed")))
(is (= "dcwp" (fuzzy/hanzi->initials "导出为 PDF")))
(is (= "sck" (fuzzy/hanzi->initials "删除块")))))
(deftest hanzi->initials-punctuation-not-word-boundary
(testing "punctuation inside a word is stripped, not treated as a word separator"
(is (= "b" (fuzzy/hanzi->initials "block(s)")))
(is (= "ti" (fuzzy/hanzi->initials "TODO: items")))))
(deftest hanzi->initials-numbers
(testing "contiguous digits form one word; space-separated digit groups each contribute first digit"
(is (= "1" (fuzzy/hanzi->initials "12345")))
(is (= "124" (fuzzy/hanzi->initials "1 23 45")))))
(deftest hanzi->initials-nil-and-blank
(testing "nil input returns nil; non-string input returns nil"
(is (nil? (fuzzy/hanzi->initials nil)))
(is (nil? (fuzzy/hanzi->initials 42)))))

View File

@@ -7172,6 +7172,11 @@ through@2, "through@>=2.2.7 <3":
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
tiny-pinyin@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/tiny-pinyin/-/tiny-pinyin-1.3.2.tgz#ce31f0f3afc2a80ee9df708fc7f4e914854d534a"
integrity sha512-uHNGu4evFt/8eNLldazeAM1M8JrMc1jshhJJfVRARTN3yT8HEEibofeQ7QETWQ5ISBjd6fKtTVBCC/+mGS6FpA==
tiny-typed-emitter@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"