Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

16
deps/common/.carve/config.edn vendored Normal file
View File

@@ -0,0 +1,16 @@
{:paths ["src"]
:api-namespaces [logseq.common.path
logseq.common.uuid
logseq.common.util.namespace
logseq.common.util.page-ref
logseq.common.util.block-ref
logseq.common.util
logseq.common.util.date-time
logseq.common.date
logseq.common.util.macro
logseq.common.cognito-config
logseq.common.config
logseq.common.defkeywords
logseq.common.graph-dir
logseq.common.graph-registry]
:report {:format :ignore}}

27
deps/common/.carve/ignore vendored Normal file
View File

@@ -0,0 +1,27 @@
;; API fn
logseq.common.graph/get-files
;; API fn
logseq.common.graph/read-directories
;; API fn
logseq.common.graph/get-default-graphs-dir
;; API fn
logseq.common.graph/expand-home
;; API fn
logseq.common.graph/get-db-graphs-dir
;; API fn
logseq.common.graph/get-db-based-graphs
;; API fn
logseq.common.authorization/verify-jwt
;; Profile utils
logseq.common.profile/profile-fn!
logseq.common.profile/*key->call-count
logseq.common.profile/*key->time-sum
;; API fn
logseq.common.plural/is-plural?
logseq.common.plural/is-singular?
logseq.common.plural/pluralize
;; API fn
logseq.common.version/format-version

11
deps/common/.clj-kondo/config.edn vendored Normal file
View File

@@ -0,0 +1,11 @@
{:linters
{:aliased-namespace-symbol {:level :warning}
:namespace-name-mismatch {:level :warning}
:used-underscored-binding {:level :warning}
:shadowed-var {:level :warning}
:consistent-alias
{:aliases {clojure.string string}}}
:lint-as {promesa.core/let clojure.core/let}
:skip-comments true
:output {:progress true}}

1
deps/common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.clj-kondo/.cache

52
deps/common/README.md vendored Normal file
View File

@@ -0,0 +1,52 @@
## Description
This library provides common util namespaces and resources to share between the
frontend and other non-frontend contexts. This library is not supposed to depend
on other logseq libraries. This library is compatible with ClojureScript and
with node/[nbb-logseq](https://github.com/logseq/nbb-logseq) to respectively
provide frontend and Electron/commandline functionality.
## API
This library is under the parent namespace `logseq.common`.
## Dev
This follows the practices that [the Logseq frontend
follows](/docs/dev-practices.md). Most of the same linters are used, with
configurations that are specific to this library. See [this library's CI
file](/.github/workflows/deps-common.yml) for linting examples.
### Setup
To run linters and tests, you'll want to install pnpm dependencies once:
```
pnpm install
```
### Testing
To run nbb-logseq tests:
```
# Run all tests
$ pnpm test
# List available options
$ pnpm test -H
# Run tests with :focus metadata flag
$ pnpm test -i focus
```
To run ClojureScript tests:
```
clojure -M:test
```
To auto-run ClojureScript tests while writing tests:
```
clojure -M:test -w src
```
### Managing dependencies
See [standard nbb/cljs library advice in graph-parser](/deps/graph-parser/README.md#managing-dependencies).

27
deps/common/bb.edn vendored Normal file
View File

@@ -0,0 +1,27 @@
{:min-bb-version "1.0.168"
:deps
{logseq/bb-tasks
#_{:local/root "../../../bb-tasks"}
{:git/url "https://github.com/logseq/bb-tasks"
:git/sha "7cc218286c03f093996539b8195de56a0ec5df6a"}}
:pods
{clj-kondo/clj-kondo {:version "2026.04.15"}}
:tasks
{test:load-all-namespaces-with-nbb
logseq.bb-tasks.nbb.test/load-all-namespaces
lint:large-vars
logseq.bb-tasks.lint.large-vars/-main
lint:carve
logseq.bb-tasks.lint.carve/-main
lint:ns-docstrings
logseq.bb-tasks.lint.ns-docstrings/-main}
:tasks/config
{:large-vars
{:metadata-exceptions #{:large-vars/cleanup-todo}
:max-lines-count 45}}}

12
deps/common/deps.edn vendored Normal file
View File

@@ -0,0 +1,12 @@
{:paths ["src" "resources"]
:deps {com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time" ;; fork
:sha "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
funcool/promesa {:mvn/version "11.0.678"}}
:aliases
{:test {:extra-paths ["test"]
:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.1"}
org.clojure/clojurescript {:mvn/version "1.12.134"}}
:main-opts ["-m" "cljs-test-runner.main"]}
:clj-kondo
{:replace-deps {clj-kondo/clj-kondo {:mvn/version "2026.04.15"}}
:main-opts ["-m" "clj-kondo.main"]}}}

4
deps/common/nbb.edn vendored Normal file
View File

@@ -0,0 +1,4 @@
{:paths ["src" "resources"]
:deps
{io.github.nextjournal/nbb-test-runner
{:git/sha "b379325cfa5a3306180649da5de3bf5166414e71"}}}

12
deps/common/package.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@logseq/common",
"version": "1.0.0",
"packageManager": "pnpm@10.33.0",
"private": true,
"devDependencies": {
"@logseq/nbb-logseq": "git+https://github.com/logseq/nbb-logseq.git#feat-db-v34"
},
"scripts": {
"test": "pnpm exec nbb-logseq -cp test -m nextjournal.test-runner"
}
}

32
deps/common/pnpm-lock.yaml generated vendored Normal file
View File

@@ -0,0 +1,32 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@logseq/nbb-logseq':
specifier: git+https://github.com/logseq/nbb-logseq.git#feat-db-v34
version: https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb
packages:
'@logseq/nbb-logseq@https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb':
resolution: {tarball: https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb}
version: 1.2.173-feat-db-v34
engines: {node: '>=20.10.0'}
hasBin: true
import-meta-resolve@4.1.0:
resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
snapshots:
'@logseq/nbb-logseq@https://codeload.github.com/logseq/nbb-logseq/tar.gz/4d9f1382ce2cec6f39bf12df49ba425f1bd5f3fb':
dependencies:
import-meta-resolve: 4.1.0
import-meta-resolve@4.1.0: {}

View File

@@ -0,0 +1,237 @@
{:meta/version 1
;; Hide empty block properties
;; Default value: false
;; :ui/hide-empty-properties? false
;; Enable tooltip preview on hover.
;; Default value: true
:ui/enable-tooltip? true
;; Display brackets [[]] around page references.
;; Default value: true
;; :ui/show-brackets? true
;; Display all lines of a block when referencing ((block)).
;; Default value: false
:ui/show-full-blocks? false
;; Automatically expand block references when zooming in.
;; Default value: true
:ui/auto-expand-block-refs? true
;; Disable accent marks when searching.
;; After changing this setting, rebuild the search index by pressing (^C ^S).
;; Default value: true
:feature/enable-search-remove-accents? true
;; Enable flashcards.
;; Default value: true
;; :feature/enable-flashcards? true
;; Disable the journal's built-in 'Scheduled tasks and deadlines' query.
;; Default value: false
;; :feature/disable-scheduled-and-deadline-query? false
;; Specify the number of days displayed in the future for
;; the 'scheduled tasks and deadlines' query.
;; Example usage:
;; Display all scheduled and deadline blocks for the next 14 days:
;; :scheduled/future-days 14
;; Default value: 7
;; :scheduled/future-days 7
;; Specify the first day of the week.
;; Available options:
;; - integer from 0 to 6 (Monday to Sunday)
;; Default value: 6 (Sunday)
:start-of-week 6
;; Specify a custom CSS import.
;; This option takes precedence over the local `logseq/custom.css` file.
;; Example usage:
;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');"
;; Specify a custom JS import.
;; This option takes precedence over the local `logseq/custom.js` file.
;; Example usage:
;; :custom-js-url "https://cdn.logseq.com/custom.js"
;; Set bullet indentation when exporting
;; Available options:
;; - `:eight-spaces` as eight spaces
;; - `:four-spaces` as four spaces
;; - `:two-spaces` as two spaces
;; - `:tab` as a tab character (default)
;; :export/bullet-indentation :tab
;; Publish all pages within the Graph
;; Regardless of whether individual pages have been marked as public.
;; Default value: false
;; :publishing/all-pages-public? false
;; Define the default home page and sidebar status.
;; If unspecified, the journal page will be loaded on startup and the right sidebar will stay hidden.
;; The `:page` value represents the name of the page displayed at startup.
;; Available options for `:sidebar` are:
;; - "Contents" to display the Contents page in the right sidebar.
;; - A specific page name to display in the right sidebar.
;; - An array of multiple pages, e.g., ["Contents" "Page A" "Page B"].
;; If `:sidebar` remains unset, the right sidebar will stay hidden.
;; Examples:
;; 1. Set "Changelog" as the home page and display "Contents" in the right sidebar:
;; :default-home {:page "Changelog", :sidebar "Contents"}
;; 2. Set "Jun 3rd, 2021" as the home page without the right sidebar:
;; :default-home {:page "Jun 3rd, 2021"}
;; 3. Set "home" as the home page and display multiple pages in the right sidebar:
;; :default-home {:page "home", :sidebar ["Page A" "Page B"]}
;; Configure custom shortcuts.
;; Syntax:
;; 1. + indicates simultaneous key presses, e.g., `Ctrl+Shift+a`.
;; 2. A space between keys represents key chords, e.g., `t s` means
;; pressing `t` followed by `s`.
;; 3. mod refers to `Ctrl` for Windows/Linux and `Command` for Mac.
;; 4. Use false to disable a specific shortcut.
;; 5. You can define multiple bindings for a single action, e.g., ["ctrl+j" "down"].
;; The full list of configurable shortcuts is available at:
;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs
;; Example:
;; :shortcuts
;; {:editor/new-block "enter"
;; :editor/new-line "shift+enter"
;; :editor/insert-link "mod+shift+k"
;; :editor/highlight false
;; :ui/toggle-settings "t s"
;; :editor/up ["ctrl+k" "up"]
;; :editor/down ["ctrl+j" "down"]
;; :editor/left ["ctrl+h" "left"]
;; :editor/right ["ctrl+l" "right"]}
:shortcuts {}
;; Configure the behavior of pressing Enter in document mode.
;; if set to true, pressing Enter will create a new block.
;; Default value: false
:shortcut/doc-mode-enter-for-new-block? false
;; Block content larger than `block/title-max-length` will not be searchable
;; or editable for performance.
;; Default value: 10000
:block/title-max-length 10000
;; Display command documentation on hover.
;; Default value: true
:ui/show-command-doc? true
;; Display empty bullet points.
;; Default value: false
:ui/show-empty-bullets? false
;; Pre-defined :view function to use with advanced queries.
:query/views
{:pprint
(fn [r] [:pre.code (pprint r)])}
;; Advanced queries `:result-transform` function.
;; Transform the query result before displaying it.
;; Example usage for DB graphs:
;; :query/result-transforms
;; {:sort-by-priority
;; (fn [result] (sort-by (fn [h] (get h :logseq.property/priority "Z")) result))}
;; Queries will be displayed at the bottom of today's journal page.
;; Example usage:
;; :default-queries
;; {:journals []}
;; Add custom commands to the command palette
;; Example usage:
;; :commands
;; [
;; ["js" "Javascript"]
;; ["md" "Markdown"]
;; ]
:commands []
;; Enable collapsing blocks with titles but no children.
;; By default, only blocks with children can be collapsed.
;; Setting `:outliner/block-title-collapse-enabled?` to true allows collapsing
;; blocks with titles (multiple lines) and content. For example:
;; - block title
;; block content
;; Default value: false
:outliner/block-title-collapse-enabled? false
;; Macros replace texts and will make you more productive.
;; Example usage:
;; Change the :macros value below to:
;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."}
;; input "{{poem red,blue}}"
;; becomes
;; Rose is red, violet's blue. Life's ordered: Org assists you.
:macros {}
;; Configure the default expansion level for linked references.
;; For example, consider the following block hierarchy:
;; - a [[page]] (level 1)
;; - b (level 2)
;; - c (level 3)
;; - d (level 4)
;;
;; With the default value of level 2, block b will be collapsed.
;; If the level's value is set to 3, block c will be collapsed.
;; Default value: 2
:ref/default-open-blocks-level 2
;; Mobile photo upload configuration.
;; :mobile/photo
;; {:allow-editing? true
;; :quality 80}
;; Mobile features options
;; Gestures
;; Example usage:
;; :mobile
;; {:gestures/disabled-in-block-with-tags ["kanban"]}
;; Extra CodeMirror options
;; See https://codemirror.net/5/doc/manual.html#config for possible options
;; Example usage:
;; :editor/extra-codemirror-options
;; {:lineWrapping false ; Default value: false
;; :lineNumbers true ; Default value: true
;; :readOnly false} ; Default value: false
;; Enable logical outdenting
;; Default value: false
;; :editor/logical-outdenting? false
;; Prefer pasting the file when text and a file are in the clipboard.
;; Default value: false
;; :editor/preferred-pasting-file? false
;; Quick capture templates for receiving content from other apps.
;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
;; by receiving content from other apps. Note: the {} cannot be omitted.
;; - {time}: capture time
;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
;; - {text}: text that users selected before sharing.
;; - {url}: URL or assets path for media files stored in Logseq.
;; You can also reorder them or use only one or two of them in the template.
;; You can also insert or format any text in the template, as shown in the following examples.
;; :quick-capture-templates
;; {:text "[[quick capture]] **{time}**: {text} from {url}"
;; :media "[[quick capture]] **{time}**: {url}"}
;; Quick capture options.
;; - insert-today? Insert the capture at the end of today's journal page (boolean).
;; - redirect-page? Redirect to the quick capture page after capturing (boolean).
;; - default-page The default page to capture to if insert-today? is false (string).
;; - prettify-url? Whether urls to YouTube or X should be wrapped with {{video ...}} or {{twitter ...}}
;; :quick-capture-options
;; {:insert-today? false ;; Default value: true
;; :redirect-page? false ;; Default value: false
;; :default-page "quick capture" ;; Default page: "quick capture"
;; :prettify-url? true} ;; Default value: true
}

View File

@@ -0,0 +1,137 @@
(ns logseq.common.authorization
"jwt token authorization"
(:require [clojure.string :as string]
[promesa.core :as p]))
(def text-decoder (js/TextDecoder.))
(def text-encoder (js/TextEncoder.))
(def ^:private jwks-ttl-ms (* 6 60 60 1000))
(def ^:private token-ttl-ms (* 60 60 1000))
(def ^:private token-capacity 200)
(defonce ^:private *jwks-cache (atom {:url nil :keys nil :fetched-at 0}))
(defonce ^:private *token-cache (atom {}))
(defn- get-now-ms []
(.now js/Date))
(defn- cached-token
[token now-s now-ms]
(when-let [{:keys [payload exp cached-at]} (get @*token-cache token)]
(when (and (number? exp)
(> exp now-s)
(< (- now-ms cached-at) token-ttl-ms))
payload)))
(defn- cache-token!
[token payload now-ms]
(let [exp (aget payload "exp")]
(when (number? exp)
(swap! *token-cache assoc token {:payload payload :exp exp :cached-at now-ms}))
(when (> (count @*token-cache) token-capacity)
(swap! *token-cache
(fn [cache]
(into {}
(remove (fn [[_ {:keys [exp cached-at]}]]
(or (not (number? exp))
(<= exp (js/Math.floor (/ now-ms 1000)))
(>= (- now-ms cached-at) token-ttl-ms))))
cache))))))
(defn- get-jwks-keys
[url & {:keys [force?]}]
(let [now (get-now-ms)
{:keys [url cached-url jwks-keys fetched-at]} {:cached-url (:url @*jwks-cache)
:url url
:jwks-keys (:keys @*jwks-cache)
:fetched-at (:fetched-at @*jwks-cache)}
fresh? (and (not force?)
(= cached-url url)
jwks-keys
(< (- now fetched-at) jwks-ttl-ms))]
(if fresh?
(p/resolved jwks-keys)
(p/let [jwks-resp (js/fetch url)
_ (when-not (.-ok jwks-resp) (throw (ex-info "jwks" {})))
jwks (.json jwks-resp)
jwks-keys (or (aget jwks "keys") #js [])]
(reset! *jwks-cache {:url url :keys jwks-keys :fetched-at now})
jwks-keys))))
(defn- base64url->uint8array [input]
(let [pad (if (pos? (mod (count input) 4))
(apply str (repeat (- 4 (mod (count input) 4)) "="))
"")
base64 (-> (str input pad)
(string/replace "-" "+")
(string/replace "_" "/"))
raw (js/atob base64)
data (js/Uint8Array. (.-length raw))]
(dotimes [i (.-length raw)]
(aset data i (.charCodeAt raw i)))
data))
(defn- decode-jwt-part [part]
(let [data (base64url->uint8array part)]
(js/JSON.parse (.decode text-decoder data))))
(defn client-id-allowed? [env client-id]
(let [primary-client-id (aget env "COGNITO_CLIENT_ID")
additional-client-ids (->> (string/split (or (aget env "COGNITO_CLIENT_IDS") "") #",")
(map string/trim)
(remove string/blank?))
allowed-client-ids (cond-> (set additional-client-ids)
(and (string? primary-client-id) (not (string/blank? primary-client-id)))
(conj primary-client-id))]
(and (string? client-id)
(not (string/blank? client-id))
(contains? allowed-client-ids client-id))))
(defn- import-rsa-key [jwk]
(.importKey js/crypto.subtle
"jwk"
jwk
#js {:name "RSASSA-PKCS1-v1_5" :hash "SHA-256"}
false
#js ["verify"]))
(defn verify-jwt [token env]
(let [parts (string/split token #"\.")]
(when (not= 3 (count parts)) (throw (ex-info "invalid" {})))
(let [header-part (nth parts 0)
payload-part (nth parts 1)
signature-part (nth parts 2)
now-ms (get-now-ms)
now-s (js/Math.floor (/ now-ms 1000))]
(if-let [cached (cached-token token now-s now-ms)]
(p/resolved cached)
(p/let [header (decode-jwt-part header-part)
payload (decode-jwt-part payload-part)
issuer (aget env "COGNITO_ISSUER")
client-id-claim (or (aget payload "aud")
(aget payload "client_id"))
_ (when (not= (aget payload "iss") issuer) (throw (ex-info "iss not found" {})))
_ (when-not (client-id-allowed? env client-id-claim) (throw (ex-info "aud not found" {})))
_ (when (and (aget payload "exp") (< (aget payload "exp") now-s))
(throw (ex-info "exp" {})))
jwks-url (aget env "COGNITO_JWKS_URL")
jwks-keys (get-jwks-keys jwks-url)
matching-key (.find jwks-keys (fn [k] (= (aget k "kid") (aget header "kid"))))
matching-key (if matching-key
matching-key
(p/let [jwks-keys (get-jwks-keys jwks-url :force? true)
matching-key (.find jwks-keys (fn [k] (= (aget k "kid") (aget header "kid"))))]
matching-key))
_ (when-not matching-key (throw (ex-info "kid" {})))
crypto-key (import-rsa-key matching-key)
data (.encode text-encoder (str header-part "." payload-part))
signature (base64url->uint8array signature-part)
ok (.verify js/crypto.subtle
"RSASSA-PKCS1-v1_5"
crypto-key
signature
data)]
(when ok
(cache-token! token payload now-ms)
payload))))))

View File

@@ -0,0 +1,13 @@
(ns logseq.common.cognito-config
"Shared Cognito configuration for frontend and CLI-safe consumers.")
(def COGNITO-CLIENT-ID
"69cs1lgme7p8kbgld8n5kseii6")
(def CLI-COGNITO-CLIENT-ID
"69cs1lgme7p8kbgld8n5kseii6")
(def OAUTH-DOMAIN
"logseq-prod.auth.us-east-1.amazoncognito.com")
(def OAUTH-SCOPE "email openid phone")

View File

@@ -0,0 +1,139 @@
(ns logseq.common.config
"Common config constants and fns that are shared between deps and app"
(:require [clojure.string :as string]))
(goog-define PUBLISHING false)
(defn hidden?
[path patterns]
(let [path (if (and (string? path)
(= \/ (first path)))
(subs path 1)
path)]
(some (fn [pattern]
(let [pattern (if (and (string? pattern)
(not= \/ (first pattern)))
(str "/" pattern)
pattern)]
(string/starts-with? (str "/" path) pattern))) patterns)))
(defn remove-hidden-files
"Removes files that match a pattern specified by :hidden config"
[files config get-path-fn]
(if-let [patterns (seq (:hidden config))]
(remove (fn [file]
(let [path (get-path-fn file)]
(hidden? path patterns))) files)
files))
(def app-name "logseq")
(defonce asset-protocol "assets://")
(defonce db-version-prefix "logseq_db_")
(defonce file-version-prefix "logseq_local_")
(defn strip-leading-db-version-prefix
"Strip exactly one leading db prefix for user-facing display values."
[s]
(if (and (string? s)
(string/starts-with? s db-version-prefix))
(subs s (count db-version-prefix))
s))
(defn canonicalize-db-version-repo
"Normalize any repo/graph name to exactly one leading db prefix."
[s]
(when (seq s)
(let [s (str s)
stripped (loop [name' s]
(if (string/starts-with? name' db-version-prefix)
(recur (subs name' (count db-version-prefix)))
name'))]
(str db-version-prefix stripped))))
(defonce default-graphs-dir "~/logseq/graphs")
(defonce local-assets-dir "assets")
(defonce unlinked-graphs-dir "Unlinked graphs")
(defonce favorites-page-name "$$$favorites")
(defonce views-page-name "$$$views")
(defonce library-page-name "Library")
(defonce quick-add-page-name "Quick add")
(defonce recycle-page-name "Recycle")
(defn local-relative-asset?
[s]
(and (string? s)
(not (string/includes? s "://"))
(re-find (re-pattern (str "^[./]*" local-assets-dir)) s)))
(defn local-protocol-asset?
[s]
(when (string? s)
(string/starts-with? s asset-protocol)))
(defn protocol-path?
[s]
(try
(let [url (js/URL. s)]
(some? (.-protocol url)))
(catch :default _
false)))
(defn remove-asset-protocol
[s]
(if (local-protocol-asset? s)
(-> s
(string/replace-first asset-protocol "file://"))
s))
(defn text-formats
[]
#{:json :org :md :yml :dat :asciidoc :rst :txt :markdown :adoc :html :js :ts :edn :clj :ml :rb :ex :erl :java :php :c :css
:tldr :sh})
(defn img-formats
[]
#{:gif :svg :jpeg :ico :png :jpg :bmp :webp :avif :cr2})
(defonce block-pattern "-")
(def unused-in-db-graphs-deprecation
"is not used in DB graphs")
(def file-only-config
"File only config keys that are deprecated in DB graphs along with
descriptions for their deprecation."
(merge
(zipmap
[:file/name-format
:file-sync/ignore-files
:hidden
:ignored-page-references-keywords
:journal/file-name-format
:journal/page-title-format
:journals-directory
:logbook/settings
:org-mode/insert-file-link?
:pages-directory
:preferred-workflow
:property/separated-by-commas
:property-pages/excludelist
:srs/learning-fraction
:srs/initial-interval
:whiteboards-directory
:feature/enable-whiteboards?]
(repeat unused-in-db-graphs-deprecation))
{:preferred-format
"is not used in DB graphs as there is only markdown mode."
:property-pages/enabled?
"is not used in DB graphs as all properties have pages"
:block-hidden-properties
"is not used in DB graphs as hiding a property is done in its configuration"
:feature/enable-block-timestamps?
"is not used in DB graphs as it is always enabled"
:favorites
"is not stored in config for DB graphs"
:default-templates
"is replaced by #Template and the `Apply template to tags` property"}))

81
deps/common/src/logseq/common/date.cljs vendored Normal file
View File

@@ -0,0 +1,81 @@
(ns logseq.common.date
"Date related fns shared by worker and frontend namespaces. Eventually some
of this should go to logseq.common.util.date-time"
(:require [cljs-time.format :as tf]
[clojure.string :as string]
[logseq.common.util :as common-util]))
(defonce built-in-journal-title-formatters
(list
"do MMM yyyy"
"do MMMM yyyy"
"MMM do, yyyy"
"MMMM do, yyyy"
"E, dd-MM-yyyy"
"E, dd.MM.yyyy"
"E, MM/dd/yyyy"
"E, yyyy/MM/dd"
"EEE, dd-MM-yyyy"
"EEE, dd.MM.yyyy"
"EEE, MM/dd/yyyy"
"EEE, yyyy/MM/dd"
"EEEE, dd-MM-yyyy"
"EEEE, dd.MM.yyyy"
"EEEE, MM/dd/yyyy"
"EEEE, yyyy/MM/dd"
"dd-MM-yyyy"
;; This tyle will mess up other date formats like "2022-08" "2022Q4" "2022/10"
;; "dd.MM.yyyy"
"MM/dd/yyyy"
"MM-dd-yyyy"
"MM_dd_yyyy"
"yyyy/MM/dd"
"yyyy-MM-dd"
"yyyy-MM-dd EEE"
"yyyy-MM-dd EEEE"
"yyyy_MM_dd"
"yyyyMMdd"
"yyyy年MM月dd日"))
(defonce slash-journal-title-formatters
(filter #(string/includes? % "/") built-in-journal-title-formatters))
(defn journal-title-formatters
[date-formatter]
(->
(cons
date-formatter
built-in-journal-title-formatters)
(distinct)))
(defn normalize-date
"Given raw date string, return a normalized date string at best effort.
Warning: this is a function with heavy cost (likely 50ms). Use with caution"
[s date-formatter]
(some
(fn [formatter]
(try
(tf/parse (tf/formatter formatter) s)
(catch :default _e
false)))
(journal-title-formatters date-formatter)))
(defn normalize-journal-title
"Normalize journal title at best effort. Return nil if title is not a valid date.
Return goog.date.Date.
Return format: 20220812T000000"
[title date-formatter]
(and title
(normalize-date (common-util/capitalize-all title) date-formatter)))
(defn ^:api valid-journal-title?
"This is a loose rule, requires double check by journal-title->custom-format.
BUG: This also accepts strings like 3/4/5 as journal titles"
[title date-formatter]
(boolean (normalize-journal-title title date-formatter)))
(defn ^:api valid-journal-title-with-slash?
[title]
(some #(valid-journal-title? title %) slash-journal-title-formatters))

View File

@@ -0,0 +1,31 @@
(ns logseq.common.defkeywords
"Macro 'defkeywords' to def keyword with config"
#?(:cljs (:require-macros [logseq.common.defkeywords])))
(def ^:private *defined-kws (volatile! {}))
#_:clj-kondo/ignore
(defmacro defkeyword
"Define keyword with docstring.
How 'find keyword definition' works?
clojure-lsp treat keywords defined by `cljs.spec.alpha/def` as keyword-definition.
Adding a :lint-as `defkeyword` -> `cljs.spec.alpha/def` in clj-kondo config make it works."
[& _args])
(defmacro defkeywords
"impl at hooks.defkeywords in .clj-kondo
(defkeywords ::a <config-map> ::b <config-map>)"
[& keyvals]
(let [kws (take-nth 2 keyvals)
current-meta (meta &form)]
(doseq [kw kws]
(when-let [info (get @*defined-kws kw)]
(when (not= (:file current-meta) (:file info))
(vswap! *defined-kws assoc kw current-meta)
(throw (ex-info "keyword already defined somewhere else" {:kw kw :info info}))))
(vswap! *defined-kws assoc kw current-meta))
;; (let [kw->config (partition 2 keyvals)]
;; (doseq [[kw config] kw->config]
;; (vswap! *defined-kw->config assoc kw config)))
)
`(vector ~@keyvals))

140
deps/common/src/logseq/common/graph.cljs vendored Normal file
View File

@@ -0,0 +1,140 @@
(ns ^:node-only logseq.common.graph
"This ns provides common fns for a graph directory and only runs in a node environment"
(:require ["fs" :as fs]
["os" :as os]
["path" :as node-path]
[clojure.string :as string]
[logseq.common.config :as common-config]
[logseq.common.graph-dir :as graph-dir]
[logseq.common.path :as path]))
(def ^:private win32?
"Copy of electron.utils/win32? . Too basic to couple the two libraries"
(= (.-platform js/process) "win32"))
(defn- fix-win-path!
"Copy of electron.utils/fix-win-path!. Too basic to couple the two libraries"
[path]
(when (not-empty path)
(if win32?
(string/replace path "\\" "/")
path)))
(defn readdir
"Reads given directory recursively and returns all filenames. Also applies
some graph-specific filtering e.g. symbolic links and files starting with '.'
are removed"
[root-dir]
(->> (tree-seq
(fn [[is-dir _fpath]]
is-dir)
(fn [[_is-dir dir]]
(let [files (fs/readdirSync dir #js {:withFileTypes true})]
(->> files
(remove #(.isSymbolicLink ^js %))
(remove #(string/starts-with? (.-name ^js %) "."))
(map #(do
[(.isDirectory %)
(node-path/join dir (.-name %))])))))
[true root-dir])
(filter (complement first))
(map second)
(map fix-win-path!)
(vec)))
(defn read-directories
"Given a dir, returns all the sub-directories"
[root-dir]
(let [files (fs/readdirSync root-dir #js {:withFileTypes true})]
(->> files
(remove #(.isSymbolicLink ^js %))
(remove #(string/starts-with? (.-name ^js %) "."))
(filter #(.isDirectory %))
(map #(.-name %)))))
(defn- path-at-or-under?
[path dir]
(or (= path dir)
(string/starts-with? path (str dir "/"))))
(defn ignored-path?
"Given a graph directory and path, returns truthy value on whether the path is
ignored. Useful for contexts like reading a graph's directory and file watcher
notifications
Rules:
- Paths starting with '.' are ignored
- Paths ending with '.DS_Store' are ignored
- Dynamic caches used by Logseq are ignored: graph-txid.edn and pages-metadata.edn
- Contents in '**/node_modules/' are ignored
- Contents in '/logseq/.recycle/' are ignored
- Contents in '/logseq/bak/' are ignored
- Contents in with '/logseq/version-files/' are ignored
- Contents in '/mirror/markdown/' are ignored
"
[dir path]
(let [dir (path/path-normalize dir)
path (path/path-normalize path)
rpath (path/trim-dir-prefix dir path)]
(when (string? path)
(or
(string/starts-with? rpath ".")
(some #(path-at-or-under? rpath %)
["logseq/.recycle" "logseq/bak" "logseq/version-files" "mirror/markdown"])
(contains? #{"logseq/graphs-txid.edn" "logseq/pages-metadata.edn"} rpath)
(some #(string/includes? rpath (str "/" % "/"))
["node_modules"])
(some #(string/ends-with? rpath %)
[".DS_Store"])
;; hidden directory or file
(or (re-find #"/\.[^.]+" rpath)
(re-find #"^\.[^.]+" rpath))))))
(def ^:private allowed-formats
#{:org :markdown :md :edn :json :js :css})
(defn- get-ext
[p]
(-> (node-path/extname p)
(subs 1)
keyword))
(defn get-files
"Given a graph's root dir, returns a list of all files that it recognizes.
Graph dir must be an absolute path in order for ignoring to work correctly"
[graph-dir]
(->> (readdir graph-dir)
(remove (partial ignored-path? graph-dir))
(filter #(contains? allowed-formats (get-ext %)))))
(defn get-default-graphs-dir
"Get default dir for storing graphs by first looking in env var."
[]
(or js/process.env.LOGSEQ_GRAPHS_DIR common-config/default-graphs-dir))
(defn expand-home
"Expands path if it starts with '~'"
[path]
(if (and (seq path) (string/starts-with? path "~"))
(node-path/join (os/homedir) (subs path 1))
path))
(defn get-db-graphs-dir
"Returns the directory where DB graphs are stored."
[]
(expand-home (get-default-graphs-dir)))
(defn get-db-based-graphs
"Returns canonical DB graph repo names from the default DB graph directory."
[]
(let [dir (get-db-graphs-dir)]
(fs/mkdirSync dir #js {:recursive true})
(->> (read-directories dir)
(remove (fn [s] (= s common-config/unlinked-graphs-dir)))
(map graph-dir/decode-graph-dir-name)
(keep (fn [s]
(when (and (string? s)
(not (string/starts-with? s common-config/file-version-prefix)))
(common-config/canonicalize-db-version-repo s))))
distinct)))

View File

@@ -0,0 +1,75 @@
(ns logseq.common.graph-dir
"Platform-agnostic graph directory naming helpers."
(:require [clojure.string :as string]
[logseq.common.config :as common-config]))
(defn encode-graph-dir-name
[graph-name]
(let [encoded (js/encodeURIComponent (or graph-name ""))]
(-> encoded
(string/replace "%20" " ")
(string/replace "~" "%7E")
(string/replace "%" "~"))))
(defn decode-graph-dir-name
[dir-name]
(when-not (and (string? dir-name)
(or (string/includes? dir-name "++")
(string/includes? dir-name "+3A+")))
(when (some? dir-name)
(try
(js/decodeURIComponent (string/replace dir-name "~" "%"))
(catch :default _
nil)))))
(def ^:private legacy-dir-pattern #"(?:\+\+|\+3A\+|%)")
(defn decode-legacy-graph-dir-name
[dir-name]
(when (and (string? dir-name)
(re-find legacy-dir-pattern dir-name))
(let [compat-name (-> dir-name
(string/replace "+3A+" ":")
(string/replace "++" "/"))]
(try
(let [decoded (js/decodeURIComponent compat-name)]
(when (seq decoded)
decoded))
(catch :default _
nil)))))
(defn repo->graph-dir-key
[repo]
(when (seq repo)
(if (string/starts-with? repo common-config/db-version-prefix)
(subs repo (count common-config/db-version-prefix))
repo)))
(defn repo-identity
"Return the canonical value used for repo identity comparison.
Repo identity comparison is based on the graph directory key, so `demo` and
`logseq_db_demo` identify the same graph. Use this helper, or `same-repo?`,
whenever code needs to decide whether two repo names represent the same graph."
[repo]
(repo->graph-dir-key repo))
(defn same-repo?
"Return true when two repo names identify the same graph."
[a b]
(let [a' (repo-identity a)
b' (repo-identity b)]
(and (some? a')
(some? b')
(= a' b'))))
(defn graph-dir-key->encoded-dir-name
[graph-dir-key]
(when (some? graph-dir-key)
(encode-graph-dir-name graph-dir-key)))
(defn repo->encoded-graph-dir-name
[repo]
(some-> repo
repo->graph-dir-key
graph-dir-key->encoded-dir-name))

View File

@@ -0,0 +1,72 @@
(ns logseq.common.graph-registry
"Graph registry normalization and lookup helpers."
(:require [clojure.string :as string]))
(def ^:private db-version-prefix
"logseq_db_")
(defn- present-string?
[s]
(and (string? s) (not (string/blank? s))))
(defn normalize-entry
[entry]
(let [local-graph-id (:local-graph-id entry)
graph-id (or (when (present-string? (:graph-id entry)) (:graph-id entry))
(when (present-string? local-graph-id) local-graph-id))]
(when-not graph-id
(throw (ex-info "Missing graph identity"
{:entry entry})))
(-> entry
(assoc :graph-id graph-id)
(dissoc :rtc-graph-id))))
(defn ^:api upsert-entry
[registry entry]
(let [entry' (normalize-entry entry)]
(->> (or registry [])
(remove #(or (= (:graph-id entry') (:graph-id %))
(and (present-string? (:repo entry'))
(= (:repo entry') (:repo %)))
(and (present-string? (:local-graph-id entry'))
(= (:local-graph-id entry') (:local-graph-id %)))))
(cons entry')
vec)))
(defn- normalize-comparable
[s]
(some-> s str string/lower-case))
(defn- canonical-repo
[s]
(when (seq s)
(let [s (str s)
stripped (loop [name' s]
(if (string/starts-with? name' db-version-prefix)
(recur (subs name' (count db-version-prefix)))
name'))]
(str db-version-prefix stripped))))
(defn- identifier-match?
[entry graph-identifier]
(let [identifier (normalize-comparable graph-identifier)
repo (normalize-comparable (:repo entry))
graph-name (normalize-comparable (:graph-name entry))
graph-id (normalize-comparable (:graph-id entry))
canonical-repo-name (normalize-comparable (canonical-repo graph-identifier))]
(or (= identifier repo)
(= identifier graph-name)
(= identifier graph-id)
(= canonical-repo-name repo))))
(defn ^:api resolve-target
[registry {:keys [graph-id graph-identifier]}]
(cond
(present-string? graph-id)
(some #(when (= graph-id (:graph-id %)) %) registry)
(present-string? graph-identifier)
(some #(when (identifier-match? % graph-identifier) %) registry)
:else
nil))

View File

@@ -0,0 +1,6 @@
(ns logseq.common.log
"Minimal, logging ns that shims lambdaisland.glogi fns for nbb. Could port
glogi to nbb later if this shim gets too big")
(defn error [& msgs]
(apply js/console.error (map clj->js msgs)))

296
deps/common/src/logseq/common/path.cljs vendored Normal file
View File

@@ -0,0 +1,296 @@
(ns logseq.common.path
"Path manipulation functions, use '/' sep on all platforms.
Also handles URL paths."
(:require [clojure.string :as string]))
(defn- safe-decode-uri-component
[uri]
(try
(.normalize (js/decodeURIComponent uri) "NFC")
(catch :default _
(js/console.error "decode-uri-component-failed" uri)
uri)))
(defn is-file-url?
[s]
(and (string? s)
(or
(string/starts-with? s "memory://") ;; special memory fs
(string/starts-with? s "assets://") ;; Electron asset, urlencoded
(string/starts-with? s "file://") ;; Electron files
)))
(defn filename
"File name of a path or URL.
Returns nil when it's a directory that ends with '/'."
[path]
(let [fname (if (string/ends-with? path "/")
nil
(last (string/split path #"/")))]
(if (and (seq fname) (is-file-url? path))
(safe-decode-uri-component fname)
fname)))
(defn split-ext
"Split file name into stem and extension, for both path and URL"
[path]
(let [fname (filename path)
pos (string/last-index-of fname ".")]
(if-not (or (nil? pos) (zero? pos))
[(subs fname 0 pos)
(string/lower-case (subs fname (+ pos 1)))]
[fname ""])))
(defn file-stem
"File name without extension"
[path]
(first (split-ext path)))
(defn file-ext
"File extension, lowercased"
[path]
(second (split-ext path)))
(defn- path-join-internal
"Joins the given path segments into a single path, handling relative paths,
'..' and '.' normalization."
[& segments]
(let [segments (remove string/blank? segments) ;; handle (path-join nil path)
segments (map #(string/replace % #"[/\\]+" "/") segments)
;; a fix for clojure.string/split
split-fn (fn [s]
(if (= s "/")
[""]
(string/split s #"/")))
join-fn (fn [segs]
(case segs
[] "."
[""] "/"
#_{:clj-kondo/ignore [:path-invalid-construct/string-join]}
(string/join "/" segs)))]
(->> (filter seq segments)
(mapcat split-fn)
(reduce (fn [acc segment]
(cond
(= "" segment)
[segment]
(= ".." segment)
(case (last acc)
".." (conj acc segment)
"" acc
nil [".."]
(pop acc))
(= "." segment)
acc
:else
(conj acc segment)))
[])
(join-fn))))
(defn- uri-path-join-internal
"Joins the given URI path segments into a single path, handling relative paths,
'..' and '.' normalization."
[& segments]
(let [segments (remove nil? segments) ;; handle (path-join nil path)
segments (map #(string/replace % #"[/\\]+" "/") segments)
;; a fix for clojure.string/split
split-fn (fn [s]
(if (= s "/")
[""]
(string/split s #"/")))
join-fn (fn [segs]
(case segs
[] "."
[""] "/"
#_{:clj-kondo/ignore [:path-invalid-construct/string-join]}
(string/join "/" segs)))]
(->> (filter seq segments)
(mapcat split-fn)
(map #(js/encodeURIComponent %))
(reduce (fn [acc segment]
(cond
(= "" segment)
[segment]
(= ".." segment)
(case (last acc)
".." (conj acc segment)
"" acc
nil [".."]
(pop acc))
(= "." segment)
acc
:else
(conj acc segment)))
[])
(join-fn))))
(defn- preserve-file-url-win-drive
[scheme encoded-path]
(cond-> encoded-path
(= scheme "file:")
(string/replace #"^/([a-zA-Z])%3[Aa](?=/|$)" "/$1:")))
(defn url-join
"Segments are not URL-encoded"
[base-url & segments]
(let [custom-scheme (re-find #"^[a-zA-Z0-9_+\-\.]+://" base-url)
custom-scheme? (and custom-scheme (not= custom-scheme "file://"))
base-url (cond-> base-url
custom-scheme? (string/replace-first custom-scheme "file://"))
^js url (try
(js/URL. (safe-decode-uri-component base-url))
(catch :default e
(js/console.error "Failed to construct URL in url-join:" base-url e)
(js/URL. "file:///")))
scheme (if custom-scheme?
(string/replace custom-scheme "//" "")
(.-protocol url))
path (.-pathname url)
domain (or (not-empty (.-host url))
(if (or custom-scheme? (string/starts-with? path "/")) "" "/"))
encoded-new-path (->> (apply uri-path-join-internal path segments)
(preserve-file-url-win-drive scheme))]
(str scheme "//" domain encoded-new-path)))
(defn path-join
"Join path segments, or URL base and path segments"
[base & segments]
(if (is-file-url? base)
(apply url-join base segments)
(let [rejoined-path (apply path-join-internal base segments)]
(if (and (not-empty base)
(string/starts-with? base "//")) ;; Win path fix
(str "/" rejoined-path)
rejoined-path))))
(defn prepend-protocol
"Prepend protocol to path. Handle UNC path. aka. path-to-url
protocol is one of file: http: https: assets:"
[protocol path]
(cond
(string/starts-with? path protocol)
(do
(js/console.error "BUG: should not prepend protocol to path with protocol" protocol path)
path)
(string/starts-with? path "//") ;; Windows UNC path
(str protocol path)
:else
(path-join (str protocol "//") path)))
(defn- path-normalize-internal
"Normalize path using path-join, break into segment and re-join"
[path]
(path-join path))
(defn url-normalize
[origin-url]
(let [^js url (try
(js/URL. (safe-decode-uri-component origin-url))
(catch :default e
(js/console.error "Failed to construct URL in url-normalize:" origin-url e)
nil))
scheme (when url (.-protocol url))
domain (when url (or (not-empty (.-host url)) "/"))
path (when url (.-pathname url))
encoded-new-path (when url
(->> (uri-path-join-internal path)
(preserve-file-url-win-drive scheme)))]
(if url
(str scheme "//" domain encoded-new-path)
origin-url)))
(defn path-normalize
"Normalize path or URL"
[path]
(-> (if (is-file-url? path)
(url-normalize path)
(path-normalize-internal path))
(.normalize "NFC")))
(defn url-to-path
"Extract path part of a URL, decoded.
The reverse operation is (path-join protocol:// path)"
[original-url]
(if (is-file-url? original-url)
;; NOTE: URL type is not consistent across all protocols
;; Check file:// and assets://, pathname behavior is different
(if-let [^js url (try
(js/URL. (string/replace (safe-decode-uri-component original-url) "assets://" "file://"))
(catch :default e
(js/console.error "Failed to construct URL in url-to-path:" original-url e)
nil))]
(let [path (.-pathname url)
host (.-host url)
path (if (string/starts-with? path "///")
(subs path 2)
path)
path (if (re-find #"(?i)^/[a-zA-Z]:" path) ;; Win path fix
(subs path 1)
path)]
(if (string/blank? host)
path
(str "//" host path)))
original-url)
original-url))
(defn file-url-or-path->path
"Converts a file URL to a local filesystem path."
[s]
(if (is-file-url? s)
(url-to-path s)
s))
(defn trim-dir-prefix
"Trim dir prefix from path"
[base-path sub-path]
(let [base-path (path-normalize base-path)
sub-path (path-normalize sub-path)
is-url? (is-file-url? base-path)]
(if (string/starts-with? sub-path base-path)
(if is-url?
(safe-decode-uri-component (string/replace (subs sub-path (count base-path)) #"^/+", ""))
(string/replace (subs sub-path (count base-path)) #"^/+", ""))
(do
(js/console.error "unhandled trim-base" base-path sub-path)
nil))))
(defn parent
"Parent, containing directory"
[path]
(if (string/includes? path "/")
;; ugly but works
(path-normalize (str path "/.."))
nil))
;; compat
(defn basename
[path]
(let [path (string/replace path #"/+$" "")]
(filename path)))
(defn absolute?
"Whether path `p` is absolute."
[p]
(let [p (path-normalize p)]
(boolean (or (is-file-url? p)
(string/starts-with? p "/")
;; is windows dir
(re-find #"^[a-zA-Z]:[/\\]" p)))))
(defn protocol-url?
"Whether path `p` is a protocol URL.
This is a loose check, it only checks if there is a valid protocol prefix."
[p]
(boolean (and (re-find #"^[a-zA-Z0-9_+\-\.]{2,}:" p) ;; HACK: avoid matching windows drive
(not (string/includes? p " ")))))

View File

@@ -0,0 +1,333 @@
(ns logseq.common.plural
"ClojureScript port of pluralize.js core (rules + API).
Usage:
(pluralize \"duck\" 2 true) ;; => \"2 ducks\"
(plural \"person\") ;; => \"people\"
(singular \"people\") ;; => \"person\"
(is-plural? \"ducks\") ;; => true
(is-singular? \"duck\") ;; => true
You can add rules at runtime:
(add-plural-rule! #\"(ox)$\" \"$1en\")
(add-uncountable-rule! \"metadata\")"
(:require [clojure.string :as string]))
;; -----------------------------------------------------------------------------
;; Rule storage (mirrors original semantics)
;; pluralize and singularize must run rules sequentially.
;; -----------------------------------------------------------------------------
(defonce ^:private plural-rules (atom [])) ;; vector of [js/RegExp replacement]
(defonce ^:private singular-rules (atom [])) ;; vector of [js/RegExp replacement]
(defonce ^:private uncountables (atom {})) ;; token -> true
(defonce ^:private irregular-plurals (atom {})) ;; plural -> singular
(defonce ^:private irregular-singles (atom {})) ;; singular -> plural
;; -----------------------------------------------------------------------------
;; Helpers
;; -----------------------------------------------------------------------------
(defn- sanitize-rule
"If rule is a string, compile to case-insensitive regexp that matches the whole string.
Else keep it (assumed to be js/RegExp)."
[rule]
(if (string? rule)
(js/RegExp. (str "^" rule "$") "i")
rule))
(defn- restore-case
"Replicate casing of `word` onto `token`."
[word token]
(cond
(= word token)
token
(= word (string/lower-case word))
(string/lower-case token)
(= word (string/upper-case word))
(string/upper-case token)
(and (seq word)
(= (subs word 0 1) (string/upper-case (subs word 0 1))))
(str (string/upper-case (subs token 0 1))
(string/lower-case (subs token 1)))
:else
(string/lower-case token)))
(defn- interpolate
"Replace $1..$12 etc in `s` using JS replace args (match, g1, g2 ...)."
[s js-args]
(.replace s (js/RegExp. "\\$(\\d{1,2})" "g")
(fn [_ idx]
(let [i (js/parseInt idx 10)
v (aget js-args i)]
(or v "")))))
(defn- replace-with-rule
"Apply a [re repl] rule to word with casing restoration (matches JS behavior)."
[word [re repl]]
(.replace word re
(fn [& args]
;; args: [match g1 g2 ... offset string]
(let [match (nth args 0)
;; In JS replace callback, second-to-last is offset
offset (nth args (- (count args) 2))
;; interpolate expects JS-ish indexed args;
;; easiest is to turn args into a JS array.
js-args (to-array args)
result (interpolate repl js-args)]
(if (= match "")
;; match empty => restore based on char before match
(restore-case (subs word (dec offset) offset) result)
(restore-case match result))))))
(defn- sanitize-word
"Return sanitized `word` based on `token` and `rules`."
[token word rules]
(cond
(or (zero? (count token))
(contains? @uncountables token))
word
:else
(let [rs rules
;; JS iterates from end to start
n (count rs)]
(loop [i (dec n)]
(if (neg? i)
word
(let [[re _ :as rule] (nth rs i)]
(if (.test re word)
(replace-with-rule word rule)
(recur (dec i)))))))))
(defn- replace-word-fn
"Build a word transformer (plural or singular)."
[replace-map-atom keep-map-atom rules-atom]
(fn [word]
(let [token (string/lower-case word)
keep-map @keep-map-atom
replace-map @replace-map-atom
rules @rules-atom]
(cond
(contains? keep-map token)
(restore-case word token)
(contains? replace-map token)
(restore-case word (get replace-map token))
:else
(sanitize-word token word rules)))))
(defn- check-word-fn
"Build a predicate for whether word is plural/singular (mirrors JS `checkWord`)."
[replace-map-atom keep-map-atom rules-atom]
(fn [word]
(let [token (string/lower-case word)
keep-map @keep-map-atom
replace-map @replace-map-atom
rules @rules-atom]
(cond
(contains? keep-map token) true
(contains? replace-map token) false
:else (= (sanitize-word token token rules) token)))))
;; -----------------------------------------------------------------------------
;; Public API (matches original surface)
;; -----------------------------------------------------------------------------
(def plural (replace-word-fn irregular-singles irregular-plurals plural-rules))
(def singular (replace-word-fn irregular-plurals irregular-singles singular-rules))
(def is-plural? (check-word-fn irregular-singles irregular-plurals plural-rules))
(def is-singular? (check-word-fn irregular-plurals irregular-singles singular-rules))
(defn pluralize
"Pluralize or singularize based on count. If inclusive, prefix with count."
([word item-count] (pluralize word item-count false))
([word item-count inclusive]
(let [pluralized (if (= item-count 1) (singular word) (plural word))]
(str (when inclusive (str item-count " "))
pluralized))))
(defn add-plural-rule!
[rule replacement]
(swap! plural-rules conj [(sanitize-rule rule) replacement]))
(defn add-singular-rule!
[rule replacement]
(swap! singular-rules conj [(sanitize-rule rule) replacement]))
(defn add-uncountable-rule!
"If word is string => mark as uncountable.
If regexp => add plural+singular passthrough rules ($0)."
[word]
(if (string? word)
(swap! uncountables assoc (string/lower-case word) true)
(do
(add-plural-rule! word "$0")
(add-singular-rule! word "$0"))))
(defn add-irregular-rule!
[single plural-word]
(let [p (string/lower-case plural-word)
s (string/lower-case single)]
(swap! irregular-singles assoc s p)
(swap! irregular-plurals assoc p s)))
;; -----------------------------------------------------------------------------
;; Data initialization (same as original JS)
;; -----------------------------------------------------------------------------
(defn- ^:large-vars/cleanup-todo init-irregulars! []
(doseq [[s p]
;; Pronouns + irregulars
[["I" "we"]
["me" "us"]
["he" "they"]
["she" "they"]
["them" "them"]
["myself" "ourselves"]
["yourself" "yourselves"]
["itself" "themselves"]
["herself" "themselves"]
["himself" "themselves"]
["themself" "themselves"]
["is" "are"]
["was" "were"]
["has" "have"]
["this" "these"]
["that" "those"]
["my" "our"]
["its" "their"]
["his" "their"]
["her" "their"]
;; Words ending with consonant + o
["echo" "echoes"]
["dingo" "dingoes"]
["volcano" "volcanoes"]
["tornado" "tornadoes"]
["torpedo" "torpedoes"]
;; Ends with us
["genus" "genera"]
["viscus" "viscera"]
;; Ends with ma
["stigma" "stigmata"]
["stoma" "stomata"]
["dogma" "dogmata"]
["lemma" "lemmata"]
["schema" "schemata"]
["anathema" "anathemata"]
;; Other irregular
["ox" "oxen"]
["axe" "axes"]
["die" "dice"]
["yes" "yeses"]
["foot" "feet"]
["eave" "eaves"]
["goose" "geese"]
["tooth" "teeth"]
["quiz" "quizzes"]
["human" "humans"]
["proof" "proofs"]
["carve" "carves"]
["valve" "valves"]
["looey" "looies"]
["thief" "thieves"]
["groove" "grooves"]
["pickaxe" "pickaxes"]
["passerby" "passersby"]
["canvas" "canvases"]]]
(add-irregular-rule! s p)))
(defn- init-plural-rules! []
(doseq [[rule repl]
[[(js/RegExp. "s?$" "i") "s"]
[(js/RegExp. "[^\\u0000-\\u007F]$" "i") "$0"]
[(js/RegExp. "([^aeiou]ese)$" "i") "$1"]
[(js/RegExp. "(ax|test)is$" "i") "$1es"]
[(js/RegExp. "(alias|[^aou]us|t[lm]as|gas|ris)$" "i") "$1es"]
[(js/RegExp. "(e[mn]u)s?$" "i") "$1s"]
[(js/RegExp. "([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$" "i") "$1"]
[(js/RegExp. "(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$" "i") "$1i"]
[(js/RegExp. "(alumn|alg|vertebr)(?:a|ae)$" "i") "$1ae"]
[(js/RegExp. "(seraph|cherub)(?:im)?$" "i") "$1im"]
[(js/RegExp. "(her|at|gr)o$" "i") "$1oes"]
[(js/RegExp. "(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$" "i") "$1a"]
[(js/RegExp. "(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$" "i") "$1a"]
[(js/RegExp. "sis$" "i") "ses"]
[(js/RegExp. "(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$" "i") "$1$2ves"]
[(js/RegExp. "([^aeiouy]|qu)y$" "i") "$1ies"]
[(js/RegExp. "([^ch][ieo][ln])ey$" "i") "$1ies"]
[(js/RegExp. "(x|ch|ss|sh|zz)$" "i") "$1es"]
[(js/RegExp. "(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$" "i") "$1ices"]
[(js/RegExp. "\\b((?:tit)?m|l)(?:ice|ouse)$" "i") "$1ice"]
[(js/RegExp. "(pe)(?:rson|ople)$" "i") "$1ople"]
[(js/RegExp. "(child)(?:ren)?$" "i") "$1ren"]
[(js/RegExp. "eaux$" "i") "$0"]
[(js/RegExp. "m[ae]n$" "i") "men"]
["thou" "you"]]]
(add-plural-rule! rule repl)))
(defn- init-singular-rules! []
(doseq [[rule repl]
[[(js/RegExp. "s$" "i") ""]
[(js/RegExp. "(ss)$" "i") "$1"]
[(js/RegExp. "(wi|kni|(?:after|half|high|low|mid|non|night|[^\\w]|^)li)ves$" "i") "$1fe"]
[(js/RegExp. "(ar|(?:wo|[ae])l|[eo][ao])ves$" "i") "$1f"]
[(js/RegExp. "ies$" "i") "y"]
[(js/RegExp. "(dg|ss|ois|lk|ok|wn|mb|th|ch|ec|oal|is|ck|ix|sser|ts|wb)ies$" "i") "$1ie"]
[(js/RegExp. "\\b(l|(?:neck|cross|hog|aun)?t|coll|faer|food|gen|goon|group|hipp|junk|vegg|(?:pork)?p|charl|calor|cut)ies$" "i") "$1ie"]
[(js/RegExp. "\\b(mon|smil)ies$" "i") "$1ey"]
[(js/RegExp. "\\b((?:tit)?m|l)ice$" "i") "$1ouse"]
[(js/RegExp. "(seraph|cherub)im$" "i") "$1"]
[(js/RegExp. "(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$" "i") "$1"]
[(js/RegExp. "(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$" "i") "$1sis"]
[(js/RegExp. "(movie|twelve|abuse|e[mn]u)s$" "i") "$1"]
[(js/RegExp. "(test)(?:is|es)$" "i") "$1is"]
[(js/RegExp. "(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$" "i") "$1us"]
[(js/RegExp. "(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$" "i") "$1um"]
[(js/RegExp. "(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$" "i") "$1on"]
[(js/RegExp. "(alumn|alg|vertebr)ae$" "i") "$1a"]
[(js/RegExp. "(cod|mur|sil|vert|ind)ices$" "i") "$1ex"]
[(js/RegExp. "(matr|append)ices$" "i") "$1ix"]
[(js/RegExp. "(pe)(rson|ople)$" "i") "$1rson"]
[(js/RegExp. "(child)ren$" "i") "$1"]
[(js/RegExp. "(eau)x?$" "i") "$1"]
[(js/RegExp. "men$" "i") "man"]]]
(add-singular-rule! rule repl)))
(defn- init-uncountables! []
(doseq [w
["adulthood" "advice" "agenda" "aid" "aircraft" "alcohol" "ammo"
"analytics" "anime" "athletics" "audio" "bison" "blood" "bream"
"buffalo" "butter" "carp" "cash" "chassis" "chess" "clothing" "cod"
"commerce" "cooperation" "corps" "debris" "diabetes" "digestion" "elk"
"energy" "equipment" "excretion" "expertise" "firmware" "flounder"
"fun" "gallows" "garbage" "graffiti" "hardware" "headquarters" "health"
"herpes" "highjinks" "homework" "housework" "information" "jeans"
"justice" "kudos" "labour" "literature" "machinery" "mackerel" "mail"
"media" "mews" "moose" "music" "mud" "manga" "news" "only" "personnel"
"pike" "plankton" "pliers" "police" "pollution" "premises" "rain"
"research" "rice" "salmon" "scissors" "series" "sewage" "shambles"
"shrimp" "software" "staff" "swine" "tennis" "traffic"
"transportation" "trout" "tuna" "wealth" "welfare" "whiting"
"wildebeest" "wildlife" "you"]]
(add-uncountable-rule! w))
(doseq [re [(js/RegExp. "pok[eé]mon$" "i")
(js/RegExp. "[^aeiou]ese$" "i")
(js/RegExp. "deer$" "i")
(js/RegExp. "fish$" "i")
(js/RegExp. "measles$" "i")
(js/RegExp. "o[iu]s$" "i")
(js/RegExp. "pox$" "i")
(js/RegExp. "sheep$" "i")]]
(add-uncountable-rule! re)))
(init-irregulars!)
(init-plural-rules!)
(init-singular-rules!)
(init-uncountables!)

View File

@@ -0,0 +1,18 @@
(ns logseq.common.profile
"Utils for profiling")
(defmacro profile-fn!
[f & {:keys [print-on-call? gen-k-fn]
:or {print-on-call? true}}]
`(let [origin-f# ~f
gen-k-fn# (or ~gen-k-fn (constantly (keyword ~f)))]
(set! ~f (fn [& args#]
(let [start# (cljs.core/system-time)
r# (apply origin-f# args#)
end# (cljs.core/system-time)
k# (gen-k-fn# r#)]
(vswap! *key->call-count update k# inc)
(vswap! *key->time-sum update k# #(+ % (- end# start#)))
(when ~print-on-call?
(println "call-count:" (get @*key->call-count k#) "time-sum(ms):" (get @*key->time-sum k#)))
r#)))))

View File

@@ -0,0 +1,11 @@
(ns logseq.common.profile
"Utils for profiling"
(:require-macros [logseq.common.profile]))
(def *key->call-count
"key -> count"
(volatile! {}))
(def *key->time-sum
"docstring"
(volatile! {}))

376
deps/common/src/logseq/common/util.cljs vendored Normal file
View File

@@ -0,0 +1,376 @@
(ns logseq.common.util
"Util fns shared between the app. Util fns only rely on
clojure standard libraries."
(:require [cljs-time.coerce :as tc]
[cljs-time.core :as t]
[cljs.reader :as reader]
[clojure.edn :as edn]
[clojure.string :as string]
[goog.string :as gstring]
[logseq.common.log :as log]))
(defn safe-decode-uri-component
[uri]
(try
(js/decodeURIComponent uri)
(catch :default _
(log/error :decode-uri-component-failed uri)
uri)))
(defn path-normalize
"Normalize file path (for reading paths from FS, not required by writing)
Keep capitalization sensitivity"
[s]
(.normalize s "NFC"))
(defn remove-nils-non-nested
"remove pairs of key-value that has nil value from a map (nested not supported)."
[nm]
(into {} (remove (comp nil? second)) nm))
(defn fast-remove-nils
"remove pairs of key-value that has nil value from a coll of maps."
[nm]
(keep (fn [m] (if (map? m) (remove-nils-non-nested m) m)) nm))
(defn split-first [pattern s]
(when-let [first-index (string/index-of s pattern)]
[(subs s 0 first-index)
(subs s (+ first-index (count pattern)) (count s))]))
(defn split-last [pattern s]
(when-let [last-index (string/last-index-of s pattern)]
[(subs s 0 last-index)
(subs s (+ last-index (count pattern)) (count s))]))
(defn tag-valid?
[tag-name]
(when (string? tag-name)
(not (re-find #"[#\t\r\n]+" tag-name))))
(defn safe-subs
"Like `subs`, but clamps out-of-range indices and returns \"\" for a non-string
`s` instead of throwing. `subs` is a bare `.substring` passthrough, so a nil
`s` otherwise raises \"Cannot read properties of null\"."
([s start]
(let [c (count s)]
(safe-subs s start c)))
([s start end]
(if (string? s)
(let [c (count s)]
(subs s (min c start) (min c end)))
"")))
(defn wrapped-by
[v start end]
(and (string? v) (>= (count v) 2)
(= start (first v)) (= end (last v))))
(defn wrapped-by-quotes?
[v]
(wrapped-by v "\"" "\""))
(defn wrapped-by-parens?
[v]
(wrapped-by v "(" ")"))
(defn url?
"Test if it is a `protocol://`-style URL.
NOTE: Can not handle mailto: links, use this with caution."
[s]
(and (string? s)
(try
(not (contains? #{nil "null"} (.-origin (js/URL. s))))
(catch :default _e
false))))
(defn json->clj
[json-string]
(-> json-string
(js/JSON.parse)
(js->clj :keywordize-keys true)))
(defn zero-pad
[n]
(if (< n 10)
(str "0" n)
(str n)))
(defn remove-boundary-slashes
[s]
(when (string? s)
(let [s (if (= \/ (first s))
(subs s 1)
s)]
(if (= \/ (last s))
(subs s 0 (dec (count s)))
s))))
(defn split-namespace-pages
[title]
(let [parts (string/split title "/")]
(->>
(loop [others (rest parts)
result [(first parts)]]
(if (seq others)
(let [prev (last result)]
(recur (rest others)
(conj result (str prev "/" (first others)))))
result))
(map string/trim))))
(def url-encoded-pattern #"(?i)%[0-9a-f]{2}") ;; (?i) for case-insensitive mode
(defn page-name-sanity
"Sanitize the page-name. Unify different diacritics and other visual differences.
Two objectives:
1. To be the same as in the filesystem;
2. To be easier to search"
[page-name]
(some-> page-name
(remove-boundary-slashes)
(path-normalize)))
(defn page-name-sanity-lc
"Sanitize the query string for a page name (mandate for :block/name)"
[s]
(page-name-sanity (string/lower-case s)))
(defn safe-page-name-sanity-lc
[s]
(if (string? s)
(page-name-sanity-lc s) s))
(defn capitalize-all
[s]
(some->> (string/split s #" ")
(map string/capitalize)
(string/join " ")))
(defn distinct-by
"Copy from medley"
[f coll]
(let [step (fn step [xs seen]
(lazy-seq
((fn [[x :as xs] seen]
(when-let [s (seq xs)]
(let [fx (f x)]
(if (contains? seen fx)
(recur (rest s) seen)
(cons x (step (rest s) (conj seen fx)))))))
xs seen)))]
(step (seq coll) #{})))
(defn distinct-by-last-wins
[f col]
{:pre [(sequential? col)]}
(reverse (distinct-by f (reverse col))))
(defn normalize-format
[format]
(case (keyword format)
:md :markdown
;; default
(keyword format)))
(defn path->file-ext
[path-or-file-name]
(let [last-part (last (string/split path-or-file-name #"/"))]
(second (re-find #"(?:\.)(\w+)[^.]*$" last-part))))
(defn get-format
"File path to format keyword, :org, :markdown, etc."
[file]
(when file
(normalize-format (keyword (some-> (path->file-ext file) string/lower-case)))))
(defn get-file-ext
"Copy of frontend.util/get-file-ext. Too basic to couple to main app"
[file]
(and
(string? file)
(string/includes? file ".")
(some-> (path->file-ext file) string/lower-case)))
(defn valid-edn-keyword?
"Determine if string is a valid edn keyword"
[s]
(try
(boolean (and (= \: (first s))
(edn/read-string (str "{" s " nil}"))))
(catch :default _
false)))
(defn safe-read-string
"Reads an edn string and returns nil if it fails to parse"
([content]
(safe-read-string {} content))
([{:keys [log-error?] :or {log-error? true} :as opts} content]
(try
(reader/read-string (dissoc opts :log-error?) content)
(catch :default e
(when log-error? (log/error :parse/read-string-failed e))
nil))))
(defn safe-read-map-string
"Reads an edn map string and returns {} if it fails to parse"
([content]
(safe-read-map-string {} content))
([opts content]
(try
(reader/read-string opts content)
(catch :default e
(log/error :parse/read-string-failed e)
{}))))
(defn safe-re-find
{:malli/schema [:=> [:cat :any :string] [:or :nil :string [:vector [:maybe :string]]]]}
[pattern s]
(when-not (string? s)
;; TODO: sentry
(js/console.trace))
(when (string? s)
(re-find pattern s)))
(def uuid-pattern "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
(defonce exactly-uuid-pattern (re-pattern (str "(?i)^" uuid-pattern "$")))
(defn uuid-string?
{:malli/schema [:=> [:cat :string] :boolean]}
[s]
(boolean (safe-re-find exactly-uuid-pattern s)))
(defn format
[fmt & args]
(apply gstring/format fmt args))
(defn concat-without-nil
[& cols]
(->> (apply concat cols)
(remove nil?)))
(defn time-ms
"Current time in milliseconds"
[]
(tc/to-long (t/now)))
(defn get-page-title
[page]
(or (:block/title page)
(:block/name page)))
(defn string-join-path
"Replace all `strings/join` used to construct paths with this function to reduce lint output.
https://github.com/logseq/logseq/pull/8679"
[parts]
#_:clj-kondo/ignore
(string/join "/" parts))
(def ^:private escape-chars "\\[]{}().+*?|$^")
(defn escape-regex-chars
"Escapes characters in string `old-value"
[old-value]
(reduce (fn [acc escape-char]
(string/replace acc escape-char (str "\\" escape-char)))
old-value escape-chars))
(defn replace-ignore-case
[s old-value new-value]
(string/replace s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
(defn sort-coll-by-dependency
"Sort the elements in the collection based on dependencies.
coll: [{:id 1 :depend-on 2} {:id 2 :depend-on 3} {:id 3}]
get-elem-id-fn: :id
get-elem-dep-id-fn :depend-on
return: [{:id 3} {:id 2 :depend-on 3} {:id 1 :depend-on 2}]"
[get-elem-id-fn get-elem-dep-id-fn coll]
(let [id->elem (into {} (keep (juxt get-elem-id-fn identity)) coll)
id->dep-id (into {} (keep (juxt get-elem-id-fn get-elem-dep-id-fn)) coll)
all-ids (set (keys id->dep-id))
seen-ids (volatile! #{}) ; to check dep-cycle
sorted-ids
(loop [r []
rest-ids all-ids
id (first rest-ids)]
(if-not id
r
(if-let [dep-id (id->dep-id id)]
(let [next-id (get rest-ids dep-id)]
(if (and next-id
;; if found dep-cycle, break it
(not (contains? @seen-ids next-id)))
(do (vswap! seen-ids conj next-id)
(recur r rest-ids next-id))
(let [rest-ids* (disj rest-ids id)]
(vreset! seen-ids #{})
(recur (conj r id) rest-ids* (first rest-ids*)))))
;; not found dep-id, so this id can be put into result now
(let [rest-ids* (disj rest-ids id)]
(vreset! seen-ids #{})
(recur (conj r id) rest-ids* (first rest-ids*))))))]
(mapv id->elem sorted-ids)))
(defonce markdown-heading-pattern #"^#+\s+")
(defn clear-markdown-heading
[content]
{:pre [(string? content)]}
(string/replace-first content markdown-heading-pattern ""))
(defn block-with-timestamps
"Adds updated-at timestamp and created-at if it doesn't exist"
[block]
(let [updated-at (time-ms)
block (cond->
(assoc block :block/updated-at updated-at)
(nil? (:block/created-at block))
(assoc :block/created-at updated-at))]
block))
(defn get-timestamp
[value]
(let [now (t/now)
f t/minus]
(if (string? value)
(case value
"1 day ago"
(tc/to-long (f now (t/days 1)))
"3 days ago"
(tc/to-long (f now (t/days 3)))
"1 week ago"
(tc/to-long (f now (t/weeks 1)))
"1 month ago"
(tc/to-long (f now (t/months 1)))
"3 months ago"
(tc/to-long (f now (t/months 3)))
"1 year ago"
(tc/to-long (f now (t/years 1)))
nil)
(tc/to-long (tc/to-date value)))))
(defn keyword->string
[x]
(if (keyword? x)
(if-let [nn (namespace x)]
(str nn "/" (name x))
(name x))
x))
(defn by-sorting
[sorting]
(let [get-value+cmp
(map
(fn [{:keys [get-value asc?]}]
[get-value
(if asc? compare #(compare %2 %1))])
sorting)]
(fn [a b]
(reduce
(fn [order [get-value cmp]]
(if (zero? order)
(cmp (get-value a) (get-value b))
(reduced order)))
0 get-value+cmp))))

View File

@@ -0,0 +1,37 @@
(ns logseq.common.util.block-ref
"Core vars and util fns for block-refs"
(:require [clojure.string :as string]))
(def left-parens "Opening characters for block-ref" "((")
(def right-parens "Closing characters for block-ref" "))")
(def left-and-right-parens "Opening and closing characters for block-ref"
(str left-parens right-parens))
(def block-ref-re #"\(\(([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\)\)")
(defn get-block-ref-id
"Extracts block id from block-ref using regex"
[s]
(second (re-matches block-ref-re s)))
(defn get-string-block-ref-id
"Extracts block id from block-ref by stripping parens e.g. ((123)) -> 123.
This is a less strict version of get-block-ref-id"
[s]
(subs s 2 (- (count s) 2)))
(defn block-ref?
"Determines if string is block ref using regex"
[s]
(boolean (get-block-ref-id s)))
(defn string-block-ref?
"Determines if string is block ref by checking parens. This is less strict version
of block-ref?"
[s]
(and (string/starts-with? s left-parens)
(string/ends-with? s right-parens)))
(defn ->block-ref
"Creates block ref string given id"
[block-id]
(str left-parens block-id right-parens))

View File

@@ -0,0 +1,110 @@
(ns logseq.common.util.date-time
"cljs-time util fns for deps"
(:require [cljs-time.coerce :as tc]
[cljs-time.core :as t]
[cljs-time.format :as tf]
[clojure.string :as string]
[logseq.common.util :as common-util]))
(def ^:private yyyyMMdd-formatter (tf/formatter "yyyyMMdd"))
(def ^:api default-journal-title-formatter "MMM do, yyyy")
;; (tf/parse (tf/formatter "dd.MM.yyyy") "2021Q4") => 20040120T000000
(defn safe-journal-title-formatters
[date-formatter]
(->> [date-formatter default-journal-title-formatter "yyyy-MM-dd" "yyyy_MM_dd"]
(remove string/blank?)
distinct))
(defn journal-title->
[journal-title then-fn formatters]
(when-not (string/blank? journal-title)
(when-let [time' (->> (map
(fn [formatter]
(try
(tf/parse (tf/formatter formatter) (common-util/capitalize-all journal-title))
(catch :default _e
nil)))
formatters)
(filter some?)
first)]
(then-fn time'))))
(defn journal-title->int
[journal-title formatters]
(when journal-title
(let [journal-title (common-util/capitalize-all journal-title)]
(journal-title-> journal-title
#(parse-long (tf/unparse yyyyMMdd-formatter %))
formatters))))
(defn format
[date date-formatter]
(when date-formatter
(tf/unparse (tf/formatter date-formatter) date)))
(defn int->local-date
[day]
(let [s (str day)
year (js/parseInt (subs s 0 4))
month (dec (js/parseInt (subs s 4 6)))
day (js/parseInt (subs s 6))]
(js/Date. year month day)))
(defn int->journal-title
[day date-formatter]
(when day
(format (tf/parse yyyyMMdd-formatter (str day)) date-formatter)))
(defn- get-weekday
[date]
(.toLocaleString date "en-us" (clj->js {:weekday "long"})))
(defn- get-date
([]
(get-date (js/Date.)))
([date]
{:year (.getFullYear date)
:month (inc (.getMonth date))
:day (.getDate date)
:weekday (get-weekday date)}))
(defn year-month-day-padded
([]
(year-month-day-padded (get-date)))
([date]
(let [{:keys [year month day]} date]
{:year year
:month (common-util/zero-pad month)
:day (common-util/zero-pad day)})))
(defn ymd
([]
(ymd (js/Date.)))
([date]
(ymd date "/"))
([date sep]
(let [{:keys [year month day]} (year-month-day-padded (get-date date))]
(str year sep month sep day))))
(defn date->int
"Given a date object, returns its journal page integer"
[date]
(parse-long
(string/replace (ymd date) "/" "")))
(defn journal-day->ms
"Converts a journal's :block/journal-day integer into milliseconds"
[day]
(when day
(-> (tf/parse yyyyMMdd-formatter (str day))
(tc/to-long))))
(defn ms->journal-day
"Converts a milliseconds timestamp to the nearest :block/journal-day"
[ms]
(some->> ms
tc/from-long
t/to-default-time-zone
(tf/unparse yyyyMMdd-formatter)
parse-long))

View File

@@ -0,0 +1,38 @@
(ns logseq.common.util.macro
"Core vars and util fns for built-in macros e.g. {{query }} and user macros e.g. {{foo}}"
(:require [clojure.string :as string]))
(def left-braces "Opening characters for macro" "{{")
(def right-braces "Closing characters for macro" "}}")
(defn macro?
[*s]
(when-let [s (and (string? *s) (string/trim *s))]
(and (string/starts-with? s left-braces) (string/ends-with? s right-braces))))
(defn macro-subs
[macro-content arguments]
(loop [s macro-content
args arguments
n 1]
(if (seq args)
(recur
(string/replace s (str "$" n) (first args))
(rest args)
(inc n))
s)))
(defn- macro-expand-value
"Checks a string for a macro and expands it if there's a macro entry for it.
This is a slimmer version of macro-else-cp"
[value macros]
(if-let [[_ macro args] (and (string? value)
(seq (re-matches #"\{\{(\S+)\s+(.*)\}\}" value)))]
(if-let [content (get macros macro)]
(macro-subs content (string/split args #"\s+"))
value)
value))
(defn expand-value-if-macro
[s macros]
(if (macro? s) (macro-expand-value s macros) s))

View File

@@ -0,0 +1,27 @@
(ns logseq.common.util.namespace
"Util fns for namespace and parent features"
(:require [clojure.string :as string]
[logseq.common.util :as common-util]))
;; Only used by DB graphs
(defonce parent-char "/")
(defonce parent-re #"/")
;; Used by DB and file graphs
(defonce namespace-char "/")
(defn namespace-page?
"Used by DB and file graphs"
[page-name]
(and (string? page-name)
(string/includes? page-name namespace-char)
(not= (string/trim page-name) namespace-char)
(not (string/starts-with? page-name "../"))
(not (string/starts-with? page-name "./"))
(not (common-util/url? page-name))))
(defn get-last-part
"Get last part of a namespace page"
[page-name]
(if (namespace-page? page-name)
(last (string/split page-name parent-char))
page-name))

View File

@@ -0,0 +1,53 @@
(ns logseq.common.util.page-ref
"Core vars and util fns for page-ref. Currently this only handles a logseq
page-ref e.g. [[page name]]"
(:require ["path" :as node-path]
[clojure.string :as string]))
(def left-brackets "Opening characters for page-ref" "[[")
(def right-brackets "Closing characters for page-ref" "]]")
(def left-and-right-brackets "Opening and closing characters for page-ref"
(str left-brackets right-brackets))
;; common regular expressions
(def page-ref-re "Inner capture and doesn't match nested brackets" #"\[\[(.*?)\]\]")
(def page-ref-without-nested-re "Matches most inner nested brackets" #"\[\[([^\[\]]+)\]\]")
(def page-ref-any-re "Inner capture that matches anything between brackets" #"\[\[(.*)\]\]")
(def markdown-page-ref-re #"\[(.*)\]\(file:.*\)")
(defn get-file-basename
"Returns the basename of a file path. e.g. /a/b/c.md -> c.md"
[path]
(when-not (string/blank? path)
(.-base (node-path/parse (string/replace path "+" "/")))))
(defn page-ref?
"Determines if string is page-ref. Avoid using with format-specific page-refs e.g. org"
[s]
(and (string/starts-with? s left-brackets)
(string/ends-with? s right-brackets)))
(defn ->page-ref
"Create a page ref given a page name"
[page-name]
(str left-brackets page-name right-brackets))
(defn get-page-name
"Extracts page names from md page-refs or logseq page-refs. Only call in
contexts where md page-refs are used"
[s]
(and (string? s)
(or (when-let [[_ label _path] (re-matches markdown-page-ref-re s)]
(string/trim label))
(-> (re-matches page-ref-any-re s)
second))))
(defn get-page-name!
"Extracts page-name from page-ref and fall back to arg. Useful for when user
input may (not) be a page-ref"
[s]
(or (get-page-name s) s))
(defn page-ref-un-brackets!
[s]
(or (get-page-name s) s))

66
deps/common/src/logseq/common/uuid.cljs vendored Normal file
View File

@@ -0,0 +1,66 @@
(ns logseq.common.uuid
"uuid generators"
(:require [datascript.core :as d]))
(defn- gen-journal-page-uuid
"00000001-2024-0620-0000-000000000000
first 8 chars as type, currently only '00000001' for journal-day-page.
the remaining chars for data of this type"
[journal-day]
{:pre [(pos-int? journal-day)
(> 1 (/ journal-day 100000000))]}
(let [journal-day-str (str journal-day)
part1 (subs journal-day-str 0 4)
part2 (subs journal-day-str 4 8)]
(uuid (str "00000001" "-" part1 "-" part2 "-0000-000000000000"))))
(defn- fill-with-0
[s length]
(let [s-length (count s)]
(apply str s (repeat (- length s-length) "0"))))
(defn- gen-block-uuid
"prefix-<hash-of-db-ident>-<padding-with-0>"
[k prefix]
(let [hash-num (str (Math/abs (hash k)))
part1 (fill-with-0 (subs hash-num 0 4) 4)
part2 (fill-with-0 (subs hash-num 4 8) 4)
part3 (fill-with-0 (subs hash-num 8 12) 4)
part4 (fill-with-0 (subs hash-num 12) 12)]
(uuid (str prefix "-" part1 "-" part2 "-" part3 "-" part4))))
(defn- gen-db-ident-block-uuid
"00000002-<hash-of-db-ident>-<padding-with-0>"
[db-ident]
{:pre [(keyword? db-ident)]}
(gen-block-uuid db-ident "00000002"))
(defn gen-uuid
"supported type:
- :journal-page-uuid, 00000001
- :db-ident-block-uuid, 00000002
- :migrate-new-block-uuid, 00000003
- :builtin-block-uuid, 00000004
- :view-block-uuid, 00000006"
([] (d/squuid))
([type' v]
(assert (some? v))
(case type'
:journal-page-uuid (gen-journal-page-uuid v)
:db-ident-block-uuid (gen-db-ident-block-uuid v)
:migrate-new-block-uuid (gen-block-uuid v "00000003")
:builtin-block-uuid (gen-block-uuid v "00000004")
:view-block-uuid (gen-block-uuid v "00000006"))))
(defn gen-journal-template-block
"Persistent uuid for journal template block"
[journal-uuid template-block-uuid]
(assert (uuid? journal-uuid) (str journal-uuid))
(assert (uuid? template-block-uuid) (str template-block-uuid))
(uuid
(str "00000005"
"-"
;; journal day
(subs (str journal-uuid) 9 23)
;; template block uuid
(subs (str template-block-uuid) 23))))

View File

@@ -0,0 +1,18 @@
(ns logseq.common.version
"Version metadata helpers for build time and revision strings.")
(goog-define BUILD_TIME "unknown")
(goog-define REVISION "dev")
(defn build-time
[]
BUILD_TIME)
(defn revision
[]
REVISION)
(defn format-version
[]
(str "Build time: " (build-time) "\n"
"Revision: " (revision)))

View File

@@ -0,0 +1,34 @@
(ns logseq.common.config-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.common.config :as common-config]))
(deftest remove-hidden-files
(let [files ["pages/foo.md" "pages/bar.md"
"script/README.md" "script/config.edn"
"dev/README.md" "dev/config.edn"]]
(is (= ["pages/foo.md" "pages/bar.md"]
(common-config/remove-hidden-files
files
{:hidden ["script" "/dev"]}
identity))
"Removes hidden relative files")
(is (= ["/pages/foo.md" "/pages/bar.md"]
(common-config/remove-hidden-files
(map #(str "/" %) files)
{:hidden ["script" "/dev"]}
identity))
"Removes hidden files if they start with '/'")))
(deftest local-relative-asset?
(testing "local-relative-asset?"
(is (common-config/local-relative-asset? "assets/test.png"))
(is (common-config/local-relative-asset? "../assets/test.png"))
(is (common-config/local-relative-asset? "./assets/test.png"))
(is (not (common-config/local-relative-asset? "assets://")))
(is (not (common-config/local-relative-asset? "assets://test.png")))
(is (not (common-config/local-relative-asset? "http://assets/test.png")))
(is (not (common-config/local-relative-asset? "file://assets/test.png")))
(is (not (common-config/local-relative-asset? nil))))
(testing "Windows backslash paths"
(is (common-config/local-relative-asset? "assets\\test.png"))))

View File

@@ -0,0 +1,50 @@
(ns logseq.common.graph-test
(:require [logseq.common.graph :as common-graph]
[cljs.test :refer [deftest is use-fixtures async]]
["fs" :as fs]
["path" :as node-path]))
(use-fixtures
:each
;; Cleaning tmp/ before leaves last tmp/ after a test run for dev and debugging
{:before
#(async done
(if (fs/existsSync "tmp")
(fs/rm "tmp" #js {:recursive true} (fn [err]
(when err (js/console.log err))
(done)))
(done)))})
(defn- create-logseq-graph
"Creates a minimal mock graph"
[dir]
(fs/mkdirSync (node-path/join dir "logseq") #js {:recursive true})
(fs/mkdirSync (node-path/join dir "journals"))
(fs/mkdirSync (node-path/join dir "pages")))
(deftest get-files
(create-logseq-graph "tmp/test-graph")
;; Create files that are recognized
(fs/writeFileSync "tmp/test-graph/pages/foo.md" "")
(fs/writeFileSync "tmp/test-graph/journals/2023_05_09.md" "")
(fs/mkdirSync (node-path/join "tmp/test-graph" "mirror" "markdown-notes") #js {:recursive true})
(fs/mkdirSync (node-path/join "tmp/test-graph" "mirror" "markdown2") #js {:recursive true})
(fs/writeFileSync "tmp/test-graph/mirror/markdown-notes/foo.md" "")
(fs/writeFileSync "tmp/test-graph/mirror/markdown2/foo.md" "")
;; Create files that are ignored
(fs/mkdirSync (node-path/join "tmp/test-graph" "logseq" "bak"))
(fs/mkdirSync (node-path/join "tmp/test-graph" "mirror" "markdown" "pages") #js {:recursive true})
(fs/writeFileSync "tmp/test-graph/logseq/bak/baz.md" "")
(fs/writeFileSync "tmp/test-graph/logseq/.gitignore" "")
(fs/writeFileSync "tmp/test-graph/mirror/markdown/pages/foo.md" "")
(is (= ["tmp/test-graph/journals/2023_05_09.md"
"tmp/test-graph/mirror/markdown-notes/foo.md"
"tmp/test-graph/mirror/markdown2/foo.md"
"tmp/test-graph/pages/foo.md"]
(common-graph/get-files "tmp/test-graph"))))
(deftest ignored-markdown-mirror-path-honors-directory-boundary-test
(is (common-graph/ignored-path? "tmp/test-graph" "tmp/test-graph/mirror/markdown"))
(is (common-graph/ignored-path? "tmp/test-graph" "tmp/test-graph/mirror/markdown/pages/foo.md"))
(is (not (common-graph/ignored-path? "tmp/test-graph" "tmp/test-graph/mirror/markdown-notes/foo.md")))
(is (not (common-graph/ignored-path? "tmp/test-graph" "tmp/test-graph/mirror/markdown2/foo.md"))))

View File

@@ -0,0 +1,73 @@
(ns logseq.common.path-test
(:require [cljs.test :refer [deftest is testing]]
[logseq.common.path :as path]))
(deftest filename
(is (= nil (path/filename "/path/to/dir/")))
(is (= "file-name" (path/filename "/path/to/dir/file-name")))
(is (= "file-name" (path/filename "dir/file-name"))))
(deftest split-ext
(is (= ["some-song" "mp3"] (path/split-ext "some-song.MP3")))
(is (= ["some-song" ""] (path/split-ext "some-song")))
(is (= ["some-file.edn" "txt"] (path/split-ext "some-file.edn.txt"))))
(deftest path-join
(testing "path-join"
(is (= "foo/bar" (path/path-join "foo" "bar")))
(is (= "foo/bar" (path/path-join "foo/" "bar")))
(is (= "foo/bar" (path/path-join nil "foo" "bar"))
"global dir")
(is (= "/foo/bar/baz/asdf" (path/path-join "/foo/bar//baz/asdf/quux/..")))
(is (= "assets:///foo.bar/baz" (path/path-join "assets:///foo.bar" "baz")))
(is (= "assets:///foo.bar/baz" (path/path-join "assets:///foo.bar/" "baz")))
(is (= "file:///D:/a.txt" (path/path-join "file://" "D:/a.txt")))
(is (= "file:///c:/x" (path/path-join "file://" "c:/x")))
(is (= "//NAS/MyGraph/logseq/config.edn" (path/path-join "//NAS/MyGraph" "logseq/config.edn")))))
(deftest prepend-protocol
(testing "prepend-protocol"
(is (= "file:///home/logseq/graph" (path/prepend-protocol "file:" "/home/logseq/graph")))
(is (= "file:///C:/Graph/pages" (path/prepend-protocol "file:" "C:/Graph/pages")))
(is (= "file://NAS/MyGraph" (path/prepend-protocol "file:" "//NAS/MyGraph"))
"Windows UNC URL")))
(deftest file-url-or-path->path
(testing "file URL paths"
(is (= "D:/a.txt" (path/file-url-or-path->path "file:///D:/a.txt")))
(is (= "c:/x" (path/file-url-or-path->path "file:///c:/x")))
(is (= "//nas/share/x" (path/file-url-or-path->path "file://NAS/share/x")))
(is (= "D:/a.txt" (path/file-url-or-path->path "file:///D%3A/a.txt")))
(is (= "/home/admin/a.txt" (path/file-url-or-path->path "file:///home/admin/a.txt")))
(is (= "/D:/a.txt" (path/file-url-or-path->path "/D:/a.txt")))
(is (= "/D:\\a.txt" (path/file-url-or-path->path "/D:\\a.txt")))
(is (= "/home/admin/a.txt" (path/file-url-or-path->path "/home/admin/a.txt")))))
(deftest path-absolute
(testing "absolute"
(is (true? (path/absolute? "D:\\sources\\sources.md")))
(is (true? (path/absolute? "/home/xxx/logseq/test.md")))
(is (false? (path/absolute? "logseq/test.md")))
(is (false? (path/absolute? "test.md")))
(is (false? (path/absolute? "test")))
(is (false? (path/absolute? "D:test.md")))))
(deftest protocol-url
(testing "protocol url"
(is (true? (path/protocol-url? "mailto:help@logseq.com")))
(is (true? (path/protocol-url? "https://logseq.com")))
(is (true? (path/protocol-url? "ftp://logseq.com")))
(is (true? (path/protocol-url? "file:///home/xxx/logseq/test.md")))
(is (true? (path/protocol-url? "assets:///home/xxx/logseq/test.md")))
(is (false? (path/protocol-url? "logseq/test.md")))
(is (false? (path/protocol-url? "test.md")))
(is (false? (path/protocol-url? "test")))
(is (false? (path/protocol-url? "D:test.md")))))
(deftest invalid-or-bare-protocol-urls
(testing "invalid or bare protocol URLs do not crash"
(is (string? (path/url-to-path "assets://")))
(is (string? (path/url-to-path "file://")))
(is (= "assets:///foo" (path/path-join "assets://" "foo")))
(is (string? (path/path-join "file://" "assets://")))
(is (string? (path/url-normalize "assets://")))))

View File

@@ -0,0 +1,11 @@
(ns logseq.common.util.page-ref-test
(:require [logseq.common.util.page-ref :as page-ref]
[cljs.test :refer [are deftest]]))
(deftest page-ref?
(are [x y] (= (page-ref/page-ref? x) y)
"[[page]]" true
"[[another page]]" true
"[[some [[nested]] page]]" true
"[single bracket]" false
"no brackets" false))

View File

@@ -0,0 +1,68 @@
(ns logseq.common.util-test
(:require [clojure.test :refer [deftest are testing]]
[logseq.common.util :as common-util]))
(deftest valid-edn-keyword?
(are [x y]
(= (common-util/valid-edn-keyword? x) y)
":foo-bar" true
":foo!" true
":foo,bar" false
"4" false
"foo bar" false
"`property" false))
(deftest extract-file-extension?
(are [x y]
(= (common-util/path->file-ext x) y)
"foo.bar" "bar"
"foo" nil
"foo.bar.baz" "baz"
"../assets/audio.mp3" "mp3"
;; From https://www.w3.org/TR/media-frags/
"../assets/audio.mp3?t=10,20" "mp3"
"../assets/audio.mp3?t=10,20#t=10" "mp3"
"/root/Documents/audio.mp3" "mp3"
"C:\\Users\\foo\\Documents\\audio.mp3" "mp3"
"/root/Documents/audio" nil
"/root/Documents/audio." nil
"special/characters/aäääöüß.7z" "7z"
"asldk lakls .lsad" "lsad"
"中文asldk lakls .lsad" "lsad"))
(deftest url?
(are [x y]
(= (common-util/url? x) y)
"http://logseq.com" true
"prop:: value" false
"a:" false))
(deftest escape-regex-chars
(testing "ensure the result is a valid regex string"
(are [x]
(some? (re-find (re-pattern (common-util/escape-regex-chars x)) x))
"[[page-name]]"
"end-with-backslash\\"
"\\[]{}().+*?|$^")))
(deftest safe-subs
(testing "behaves like subs for in-range indices"
(are [args y]
(= (apply common-util/safe-subs args) y)
["hello" 1 3] "el"
["hello" 2] "llo"
["hello" 0 5] "hello"))
(testing "clamps out-of-range indices instead of throwing"
(are [args y]
(= (apply common-util/safe-subs args) y)
["hello" 0 99] "hello"
["hello" 99 99] ""
["hello" 99] ""))
(testing "returns \"\" for a non-string instead of throwing"
(are [args y]
(= (apply common-util/safe-subs args) y)
[nil 0 0] ""
[nil 0 5] ""
[nil 0] ""
[42 0 1] "")))