mirror of
https://github.com/logseq/logseq.git
synced 2026-05-16 17:02:34 +00:00
Merge branch 'master' into enhance/property-ux
This commit is contained in:
@@ -29,6 +29,8 @@ i18n_functions = [
|
||||
"tt",
|
||||
"i18n/t",
|
||||
"i18n/tt",
|
||||
# Parameterized translation function.
|
||||
"t-fn",
|
||||
]
|
||||
|
||||
# Alert/notification functions.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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]
|
||||
|
||||
74
src/test/frontend/common/search_fuzzy_test.cljs
Normal file
74
src/test/frontend/common/search_fuzzy_test.cljs
Normal 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)))))
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user