Browse Source

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 years ago
parent
commit
5c2e644a43
100 changed files with 3533 additions and 1557 deletions
  1. 1 3
      .carve/config.edn
  2. 6 11
      .clj-kondo/config.edn
  3. 1 3
      .github/workflows/build.yml
  4. 113 0
      .github/workflows/graph-parser.yml
  5. 1 0
      .gitignore
  6. 5 1
      CODEBASE_OVERVIEW.md
  7. 2 2
      android/app/build.gradle
  8. 2 0
      android/app/capacitor.build.gradle
  9. 8 0
      android/app/src/main/assets/capacitor.plugins.json
  10. 6 0
      android/capacitor.settings.gradle
  11. 19 3
      bb.edn
  12. 3 3
      deps.edn
  13. 9 0
      deps/graph-parser/.carve/config.edn
  14. 4 0
      deps/graph-parser/.carve/ignore
  15. 13 0
      deps/graph-parser/.clj-kondo/config.edn
  16. 3 0
      deps/graph-parser/.gitignore
  17. 63 0
      deps/graph-parser/README.md
  18. 23 0
      deps/graph-parser/deps.edn
  19. 11 0
      deps/graph-parser/package.json
  20. 70 0
      deps/graph-parser/src/logseq/graph_parser.cljs
  21. 9 3
      deps/graph-parser/src/logseq/graph_parser/block.cljc
  22. 68 0
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  23. 60 0
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  24. 2 2
      deps/graph-parser/src/logseq/graph_parser/date_time_util.cljs
  25. 15 0
      deps/graph-parser/src/logseq/graph_parser/db.cljs
  26. 1 1
      deps/graph-parser/src/logseq/graph_parser/db/default.cljs
  27. 1 1
      deps/graph-parser/src/logseq/graph_parser/db/schema.cljs
  28. 60 53
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  29. 0 0
      deps/graph-parser/src/logseq/graph_parser/log.cljs
  30. 1 1
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  31. 2 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  32. 160 0
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  33. 1 119
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  34. 1 6
      deps/graph-parser/src/logseq/graph_parser/utf8.cljs
  35. 2 5
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  36. 2 1
      deps/graph-parser/test/logseq/graph_parser/block_test.cljs
  37. 13 0
      deps/graph-parser/test/logseq/graph_parser/cli_test.cljs
  38. 17 32
      deps/graph-parser/test/logseq/graph_parser/extract_test.cljs
  39. 7 6
      deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs
  40. 4 8
      deps/graph-parser/test/logseq/graph_parser/nbb_test_runner.cljs
  41. 0 0
      deps/graph-parser/test/logseq/graph_parser/property_test.cljs
  42. 0 30
      deps/graph-parser/test/logseq/graph_parser/text_test.cljs
  43. 411 0
      deps/graph-parser/yarn.lock
  44. 2 2
      docs/contributing-to-translations.md
  45. 7 1
      docs/dev-practices.md
  46. 1 1
      docs/mobile.md
  47. 3 3
      e2e-tests/sidebar.spec.ts
  48. 6 6
      ios/App/App.xcodeproj/project.pbxproj
  49. 1 0
      ios/App/Podfile
  50. 4 2
      package.json
  51. 3 3
      resources/css/common.css
  52. 9 0
      resources/css/shepherd.css
  53. 117 0
      resources/js/shepherd.min.js
  54. 2 2
      resources/package.json
  55. 1 0
      scripts/bump-version.sh
  56. 1 1
      scripts/carve.clj
  57. 9 5
      scripts/large_vars.clj
  58. 1 1
      scripts/src/logseq/tasks/dev.clj
  59. 25 8
      scripts/src/logseq/tasks/nbb.clj
  60. 66 19
      src/electron/electron/core.cljs
  61. 41 22
      src/electron/electron/fs_watcher.cljs
  62. 85 29
      src/main/frontend/components/block.cljs
  63. 39 1
      src/main/frontend/components/block.css
  64. 5 11
      src/main/frontend/components/editor.cljs
  65. 6 3
      src/main/frontend/components/editor.css
  66. 2 1
      src/main/frontend/components/export.cljs
  67. 4 4
      src/main/frontend/components/file.cljs
  68. 26 18
      src/main/frontend/components/header.cljs
  69. 108 101
      src/main/frontend/components/header.css
  70. 2 2
      src/main/frontend/components/journal.cljs
  71. 557 334
      src/main/frontend/components/onboarding/index.css
  72. 158 0
      src/main/frontend/components/onboarding/quick_tour.cljs
  73. 155 150
      src/main/frontend/components/page.cljs
  74. 15 15
      src/main/frontend/components/page.css
  75. 2 1
      src/main/frontend/components/reference.cljs
  76. 5 5
      src/main/frontend/components/repo.cljs
  77. 1 1
      src/main/frontend/components/right_sidebar.cljs
  78. 3 3
      src/main/frontend/components/select.cljs
  79. 9 0
      src/main/frontend/components/settings.cljs
  80. 117 72
      src/main/frontend/components/sidebar.cljs
  81. 71 37
      src/main/frontend/components/sidebar.css
  82. 2 2
      src/main/frontend/components/svg.cljs
  83. 14 38
      src/main/frontend/config.cljs
  84. 23 32
      src/main/frontend/db.cljs
  85. 7 26
      src/main/frontend/db/conn.cljs
  86. 5 88
      src/main/frontend/db/model.cljs
  87. 13 11
      src/main/frontend/db/query_dsl.cljs
  88. 5 3
      src/main/frontend/db/query_react.cljs
  89. 0 15
      src/main/frontend/db/react.cljs
  90. 363 20
      src/main/frontend/dicts.cljc
  91. 2 2
      src/main/frontend/diff.cljs
  92. 23 19
      src/main/frontend/extensions/calc.cljc
  93. 3 0
      src/main/frontend/extensions/code.cljs
  94. 179 124
      src/main/frontend/extensions/html_parser.cljs
  95. 12 8
      src/main/frontend/extensions/pdf/assets.cljs
  96. 4 0
      src/main/frontend/extensions/pdf/pdf.css
  97. 6 3
      src/main/frontend/extensions/sci.cljs
  98. 1 1
      src/main/frontend/extensions/zotero/extractor.cljs
  99. 0 5
      src/main/frontend/format.cljs
  100. 2 1
      src/main/frontend/format/block.cljs

+ 1 - 3
.carve/config.edn

@@ -5,7 +5,5 @@
                   ;; Ignore b/c too many false positives
                   ;; Ignore b/c too many false positives
                   frontend.db
                   frontend.db
                   ;; Used for debugging
                   ;; Used for debugging
-                  frontend.db.debug
-                  ;; carve doesn't detect nbb only usage
-                  logseq.graph-parser.log]
+                  frontend.db.debug]
  :report {:format :ignore}}
  :report {:format :ignore}}

+ 6 - 11
.clj-kondo/config.edn

@@ -2,13 +2,7 @@
  {:unresolved-symbol {:exclude [goog.DEBUG
  {:unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
                                 ;; TODO:lint: Fix when fixing all type hints
-                                object
-                                ;; TODO: Remove parse-* and update-* when https://github.com/clj-kondo/clj-kondo/issues/1694 is done
-                                parse-long
-                                parse-double
-                                parse-uuid
-                                update-keys
-                                update-vals]}
+                                object]}
   ;; TODO:lint: Remove node-path excludes once we have a cleaner api
   ;; TODO:lint: Remove node-path excludes once we have a cleaner api
   :unresolved-var {:exclude [frontend.util/node-path.basename
   :unresolved-var {:exclude [frontend.util/node-path.basename
                              frontend.util/node-path.dirname
                              frontend.util/node-path.dirname
@@ -26,10 +20,12 @@
              frontend.db.query-react query-react
              frontend.db.query-react query-react
              frontend.util util
              frontend.util util
              frontend.util.property property
              frontend.util.property property
+             frontend.util.text text-util
              frontend.config config
              frontend.config config
              frontend.format.mldoc mldoc
              frontend.format.mldoc mldoc
              frontend.format.block block
              frontend.format.block block
              frontend.handler.extract extract
              frontend.handler.extract extract
+             logseq.graph-parser graph-parser
              logseq.graph-parser.text text
              logseq.graph-parser.text text
              logseq.graph-parser.block gp-block
              logseq.graph-parser.block gp-block
              logseq.graph-parser.mldoc gp-mldoc
              logseq.graph-parser.mldoc gp-mldoc
@@ -39,12 +35,10 @@
              logseq.graph-parser.date-time-util date-time-util}}}
              logseq.graph-parser.date-time-util date-time-util}}}
 
 
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
-                         rum.core/defcs hooks.rum/defcs}}
+                        rum.core/defcs hooks.rum/defcs}}
  :lint-as {promesa.core/let clojure.core/let
  :lint-as {promesa.core/let clojure.core/let
            promesa.core/loop clojure.core/loop
            promesa.core/loop clojure.core/loop
            promesa.core/recur clojure.core/recur
            promesa.core/recur clojure.core/recur
-           garden.def/defstyles clojure.core/def
-           garden.def/defkeyframes clojure.core/def
            rum.core/defcc rum.core/defc
            rum.core/defcc rum.core/defc
            rum.core/defcontext clojure.core/def
            rum.core/defcontext clojure.core/def
            clojure.test.check.clojure-test/defspec clojure.core/def
            clojure.test.check.clojure-test/defspec clojure.core/def
@@ -55,4 +49,5 @@
            frontend.namespaces/import-vars potemkin/import-vars
            frontend.namespaces/import-vars potemkin/import-vars
            ;; src/test
            ;; src/test
            frontend.react/defc clojure.core/defn}
            frontend.react/defc clojure.core/defn}
- :skip-comments true}
+ :skip-comments true
+ :output {:progress true}}

+ 1 - 3
.github/workflows/build.yml

@@ -79,11 +79,9 @@ jobs:
           yarn cljs:test
           yarn cljs:test
           node static/tests.js
           node static/tests.js
 
 
-      - name: Run nbb tests for graph-parser
-        run: yarn nbb-logseq -cp src/main:src/test -m logseq.graph-parser.nbb-test-runner/run-tests
       # In this job because it depends on an npm package
       # In this job because it depends on an npm package
       - name: Load nbb compatible namespaces
       - name: Load nbb compatible namespaces
-        run: bb test:load-nbb-compatible-namespaces
+        run: bb test:load-namespaces-with-nbb
 
 
   lint:
   lint:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 113 - 0
.github/workflows/graph-parser.yml

@@ -0,0 +1,113 @@
+name: logseq graph-parser CI
+
+on:
+  # Path filters ensure jobs only kick off if a change is made to graph-parser
+  push:
+    branches: [master]
+    paths:
+      - 'deps/graph-parser/**'
+      - '!deps/graph-parser/**.md'
+  pull_request:
+    branches: [master]
+    paths:
+      - 'deps/graph-parser/**'
+      - '!deps/graph-parser/**.md'
+
+env:
+  CLOJURE_VERSION: '1.10.1.727'
+  # setup-java@v2 dropped support for legacy Java version syntax.
+  # This is the same as 1.8.
+  JAVA_VERSION: '8'
+  # This is the latest node version we can run.
+  NODE_VERSION: '16'
+  BABASHKA_VERSION: '0.8.2'
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Set up Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          cache: 'yarn'
+          cache-dependency-path: deps/graph-parser/yarn.lock
+
+      - name: Set up Java
+        uses: actions/setup-java@v2
+        with:
+          distribution: 'zulu'
+          java-version: ${{ env.JAVA_VERSION }}
+
+      - name: Set up Clojure
+        uses: DeLaGuardo/setup-clojure@master
+        with:
+          cli: ${{ env.CLOJURE_VERSION }}
+
+      - name: Setup Babashka
+        uses: turtlequeue/[email protected]
+        with:
+          babashka-version: ${{ env.BABASHKA_VERSION }}
+
+      - name: Clojure cache
+        uses: actions/cache@v2
+        id: clojure-deps
+        with:
+          path: |
+            ~/.m2/repository
+            ~/.gitlibs
+          key: ${{ runner.os }}-clojure-deps-${{ hashFiles('deps.edn') }}
+          restore-keys: ${{ runner.os }}-clojure-deps-
+
+      - name: Fetch Clojure deps
+        if: steps.clojure-deps.outputs.cache-hit != 'true'
+        run: cd deps/graph-parser && clojure -A:test -P
+
+      - name: Fetch yarn deps
+        run: cd deps/graph-parser && yarn install --frozen-lockfile
+
+      - name: Run ClojureScript tests
+        run: cd deps/graph-parser && clojure -M:test
+
+      - name: Run nbb-logseq tests
+        run: cd deps/graph-parser && yarn nbb-logseq -cp src:test -m logseq.graph-parser.nbb-test-runner/run-tests
+
+      # In this job because it depends on an npm package
+      - name: Load namespaces into nbb-logseq
+        run: bb test:load-all-namespaces-with-nbb deps/graph-parser src
+
+  lint:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Set up Java
+        uses: actions/setup-java@v2
+        with:
+          distribution: 'zulu'
+          java-version: ${{ env.JAVA_VERSION }}
+
+      - name: Set up Clojure
+        uses: DeLaGuardo/setup-clojure@master
+        with:
+          cli: ${{ env.CLOJURE_VERSION }}
+
+      - name: Setup Babashka
+        uses: turtlequeue/[email protected]
+        with:
+          babashka-version: ${{ env.BABASHKA_VERSION }}
+
+      - name: Run clj-kondo lint
+        run: cd deps/graph-parser && clojure -M:clj-kondo --parallel --lint src test
+
+      - name: Carve lint for unused vars
+        run: cd deps/graph-parser && ../../scripts/carve.clj
+
+      - name: Lint for vars that are too large
+        run: scripts/large_vars.clj deps/graph-parser/src '{:max-lines-count 75}'

+ 1 - 0
.gitignore

@@ -44,3 +44,4 @@ ios/App/App/capacitor.config.json
 startup.png
 startup.png
 
 
 /src/test/docs
 /src/test/docs
+~*~

+ 5 - 1
CODEBASE_OVERVIEW.md

@@ -46,7 +46,11 @@ After cloning the [Logseq repository](https://github.com/logseq/logseq), there a
 
 
   - `src/main/frontend/` contains code that powers the Logseq editor. Folders and files inside are organized by features or functions. For example, `components` contains all the UI components and `handler` contains all the event-handling code. You can explore on your own interest.
   - `src/main/frontend/` contains code that powers the Logseq editor. Folders and files inside are organized by features or functions. For example, `components` contains all the UI components and `handler` contains all the event-handling code. You can explore on your own interest.
 
 
-  - `src/main/logseq/` contains the api used by plugins and the graph-parser.
+  - `src/main/logseq/` contains the api used by plugins.
+
+- `deps/` contains dependencies or libraries used by the frontend.
+
+  - `deps/graph-parser/` is a library that parses a Logseq graph and saves it to a database.
 
 
 ## Data Flow
 ## Data Flow
 
 

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 24
-        versionName "0.7.0"
+        versionCode 25
+        versionName "0.7.1"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 2 - 0
android/app/capacitor.build.gradle

@@ -11,7 +11,9 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 dependencies {
 dependencies {
     implementation project(':capacitor-app')
     implementation project(':capacitor-app')
     implementation project(':capacitor-camera')
     implementation project(':capacitor-camera')
+    implementation project(':capacitor-clipboard')
     implementation project(':capacitor-filesystem')
     implementation project(':capacitor-filesystem')
+    implementation project(':capacitor-haptics')
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-share')
     implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-splash-screen')

+ 8 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -7,10 +7,18 @@
 		"pkg": "@capacitor/camera",
 		"pkg": "@capacitor/camera",
 		"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
 		"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
 	},
 	},
+	{
+		"pkg": "@capacitor/clipboard",
+		"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
+	},
 	{
 	{
 		"pkg": "@capacitor/filesystem",
 		"pkg": "@capacitor/filesystem",
 		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
 		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
 	},
 	},
+	{
+		"pkg": "@capacitor/haptics",
+		"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
+	},
 	{
 	{
 		"pkg": "@capacitor/keyboard",
 		"pkg": "@capacitor/keyboard",
 		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
 		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"

+ 6 - 0
android/capacitor.settings.gradle

@@ -8,9 +8,15 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
 include ':capacitor-camera'
 include ':capacitor-camera'
 project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
 project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
 
 
+include ':capacitor-clipboard'
+project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
+
 include ':capacitor-filesystem'
 include ':capacitor-filesystem'
 project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
 project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
 
 
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
+
 include ':capacitor-keyboard'
 include ':capacitor-keyboard'
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 
 

+ 19 - 3
bb.edn

@@ -2,9 +2,16 @@
  :deps
  :deps
  {org.babashka/spec.alpha
  {org.babashka/spec.alpha
   {:git/url "https://github.com/babashka/spec.alpha"
   {:git/url "https://github.com/babashka/spec.alpha"
-   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}}
+   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}
+  logseq/bb-tasks
+  #_{:local/root "../bb-tasks"}
+  {:git/url "https://github.com/logseq/bb-tasks"
+   :git/sha "4b3e623fb475cacb992425aa9dac770d6dd63e38"}
+  logseq/graph-parser
+  {:local/root "deps/graph-parser"}}
  :pods
  :pods
- {clj-kondo/clj-kondo {:version "2022.02.09"}}
+ {clj-kondo/clj-kondo {:version "2022.02.09"}
+  org.babashka/fswatcher {:version "0.0.3"}}
  :tasks
  :tasks
  {dev:watch
  {dev:watch
   logseq.tasks.dev/watch
   logseq.tasks.dev/watch
@@ -26,9 +33,18 @@
   dev:lint
   dev:lint
   logseq.tasks.dev/lint
   logseq.tasks.dev/lint
 
 
-  test:load-nbb-compatible-namespaces
+  nbb:watch
+  logseq.bb-tasks.nbb.watch/watch
+
+  nbb:portal-watch
+  logseq.bb-tasks.nbb.watch/portal-watch
+
+  test:load-namespaces-with-nbb
   logseq.tasks.nbb/load-compatible-namespaces
   logseq.tasks.nbb/load-compatible-namespaces
 
 
+  test:load-all-namespaces-with-nbb
+  logseq.tasks.nbb/load-all-namespaces
+
   lang:list
   lang:list
   logseq.tasks.lang/list-langs
   logseq.tasks.lang/list-langs
 
 

+ 3 - 3
deps.edn

@@ -29,8 +29,8 @@
   camel-snake-kebab/camel-snake-kebab   {:mvn/version "0.4.2"}
   camel-snake-kebab/camel-snake-kebab   {:mvn/version "0.4.2"}
   instaparse/instaparse                 {:mvn/version "1.4.10"}
   instaparse/instaparse                 {:mvn/version "1.4.10"}
   nubank/workspaces                     {:mvn/version "1.1.1"}
   nubank/workspaces                     {:mvn/version "1.1.1"}
-  frankiesardo/linked                   {:mvn/version "1.3.0"}
-  org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}}
+  org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}
+  logseq/graph-parser                   {:local/root "deps/graph-parser"}}
 
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
@@ -47,5 +47,5 @@
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
 
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
-           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.01.13"}}
+           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.28"}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}

+ 9 - 0
deps/graph-parser/.carve/config.edn

@@ -0,0 +1,9 @@
+{:paths ["src"]
+ :api-namespaces [
+                  ;; carve doesn't detect nbb only usage
+                  logseq.graph-parser.log
+                  ;; Used by logseq but not worth splitting up
+                  logseq.graph-parser.db.schema
+                  ;; Used in tests
+                  logseq.graph-parser.test.docs-graph-helper]
+ :report {:format :ignore}}

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -0,0 +1,4 @@
+;; For CLI
+logseq.graph-parser.cli/parse-graph
+;; For CLI
+logseq.graph-parser.mldoc/ast-export-markdown

+ 13 - 0
deps/graph-parser/.clj-kondo/config.edn

@@ -0,0 +1,13 @@
+{:linters
+ {:consistent-alias
+  {:aliases {datascript.core d
+             logseq.graph-parser graph-parser
+             logseq.graph-parser.text text
+             logseq.graph-parser.block gp-block
+             logseq.graph-parser.mldoc gp-mldoc
+             logseq.graph-parser.util gp-util
+             logseq.graph-parser.property gp-property
+             logseq.graph-parser.config gp-config
+             logseq.graph-parser.date-time-util date-time-util}}}
+ :skip-comments true
+ :output {:progress true}}

+ 3 - 0
deps/graph-parser/.gitignore

@@ -0,0 +1,3 @@
+/.clj-kondo/.cache
+cljs-test-runner-out
+/test/docs

+ 63 - 0
deps/graph-parser/README.md

@@ -0,0 +1,63 @@
+## Description
+
+This library parses a logseq graph directory and returns it as a datascript
+database connection. This library powers the Logseq app and also runs from the
+commandline, _independent_ of the app. This is powerful as this can run anywhere
+that a Node.js script has access to a Logseq graph e.g. on CI processes like
+Github Actions. This library is compatible with ClojureScript and with
+[nbb-logseq](https://github.com/logseq/nbb-logseq) to respectively provide
+frontend and commandline functionality.
+
+## API
+
+This library is under the parent namespace `logseq.graph-parser`. This library
+provides two main namespaces for parsing, `logseq.graph-parser` and
+`logseq.graph-parser.cli`. `logseq.graph-parser/parse-file` is the main fn for
+the frontend. `logseq.graph-parser.cli/parse-graph` is the main fn for node.js
+CLIs.
+
+## Usage
+
+See `logseq.graph-parser.cli-test` for now. A real world example is coming soon.
+
+## 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/graph-parser.yml) for linting examples.
+
+### Setup
+
+To run linters and tests, you'll want to install yarn dependencies once:
+```
+yarn install
+```
+
+This step is not needed if you're just running the application.
+
+### Testing
+
+Since this file is compatible with cljs and nbb-logseq, tests are run against both languages.
+
+ClojureScript tests use https://github.com/Olical/cljs-test-runner. To run tests:
+```
+clojure -M:test
+```
+
+To see available options that can run specific tests or namespaces: `clojure -M:test --help`
+
+To run nbb-logseq tests:
+```
+yarn nbb-logseq -cp src:test -m logseq.graph-parser.nbb-test-runner/run-tests
+```
+
+### Managing dependencies
+
+The package.json dependencies are just for testing and should be updated if there is
+new behavior to test.
+
+The deps.edn dependecies are used by both ClojureScript and nbb-logseq. Their
+versions should be backwards compatible with each other with priority given to
+the frontend. _No new dependency_ should be introduced to this library without
+an understanding of the tradeoffs of adding this to nbb-logseq.

+ 23 - 0
deps/graph-parser/deps.edn

@@ -0,0 +1,23 @@
+{:paths ["src"]
+ :deps
+ ;; Deps should be kept in sync with https://github.com/logseq/nbb-logseq/blob/main/bb.edn
+ {datascript/datascript {:mvn/version "1.3.8"}
+  frankiesardo/linked {:mvn/version "1.3.0"}
+  com.andrewmcveigh/cljs-time {:git/url "https://github.com/logseq/cljs-time" ;; fork
+                               :sha     "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
+  ;; stubbed in nbb
+  com.lambdaisland/glogi {:mvn/version "1.1.144"}
+  ;; built in to nbb
+  cljs-bean/cljs-bean {:mvn/version "1.5.0"}}
+
+ :aliases
+ ;; This runs tests with nodejs. Would be nice to run this with in a browser env
+ ;; since this is how its normally run in the app but this requires more setup
+ ;; with karma, shadow-cljs.edn and headless mode on CI
+ {:test {:extra-paths ["test"]
+         :extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}
+                      org.clojure/clojurescript {:mvn/version "1.11.54"}}
+         :main-opts ["-m" "cljs-test-runner.main"]}
+
+  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.28"}}
+              :main-opts  ["-m" "clj-kondo.main"]}}}

+ 11 - 0
deps/graph-parser/package.json

@@ -0,0 +1,11 @@
+{
+  "name": "@logseq/graph-parser",
+  "version": "1.0.0",
+  "private": true,
+  "devDependencies": {
+    "@logseq/nbb-logseq": "^0.5.103"
+  },
+  "dependencies": {
+    "mldoc": "^1.3.3"
+  }
+}

+ 70 - 0
deps/graph-parser/src/logseq/graph_parser.cljs

@@ -0,0 +1,70 @@
+(ns logseq.graph-parser
+  "Main ns used by logseq app to parse graph from source files"
+  (:require [datascript.core :as d]
+            [logseq.graph-parser.extract :as extract]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.date-time-util :as date-time-util]
+            [logseq.graph-parser.config :as gp-config]
+            [clojure.string :as string]
+            [clojure.set :as set]))
+
+(defn- db-set-file-content!
+  "Modified copy of frontend.db.model/db-set-file-content!"
+  [conn path content]
+  (let [tx-data {:file/path path
+                 :file/content content}]
+    (d/transact! conn [tx-data] {:skip-refresh? true})))
+
+(defn parse-file
+  "Parse file and save parsed data to the given db. Main parse fn used by logseq app"
+  [conn file content {:keys [new? delete-blocks-fn extract-options]
+                      :or {new? true
+                           delete-blocks-fn (constantly [])}
+                      :as options}]
+  (db-set-file-content! conn file content)
+  (let [format (gp-util/get-format file)
+        file-content [{:file/path file}]
+        tx (if (contains? gp-config/mldoc-support-formats format)
+             (let [extract-options' (merge {:block-pattern (gp-config/get-block-pattern format)
+                                            :date-formatter "MMM do, yyyy"
+                                            :supported-formats (gp-config/supported-formats)}
+                                           extract-options
+                                           {:db @conn})
+                   [pages blocks]
+                   (extract/extract-blocks-pages file content extract-options')
+                   delete-blocks (delete-blocks-fn (first pages) file)
+                   block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks)
+                   block-refs-ids (->> (mapcat :block/refs blocks)
+                                       (filter (fn [ref] (and (vector? ref)
+                                                              (= :block/uuid (first ref)))))
+                                       (map (fn [ref] {:block/uuid (second ref)}))
+                                       (seq))
+                   ;; To prevent "unique constraint" on datascript
+                   block-ids (set/union (set block-ids) (set block-refs-ids))
+                   pages (extract/with-ref-pages pages blocks)
+                   pages-index (map #(select-keys % [:block/name]) pages)]
+               ;; does order matter?
+               (concat file-content pages-index delete-blocks pages block-ids blocks))
+             file-content)
+        tx (concat tx [(cond-> {:file/path file}
+                               new?
+                               ;; TODO: use file system timestamp?
+                               (assoc :file/created-at (date-time-util/time-ms)))])]
+    (d/transact! conn (gp-util/remove-nils tx) (select-keys options [:new-graph? :from-disk?]))))
+
+(defn filter-files
+  "Filters files in preparation for parsing. Only includes files that are
+  supported by parser"
+  [files]
+  (let [support-files (filter
+                       (fn [file]
+                         (let [format (gp-util/get-format (:file/path file))]
+                           (contains? (set/union #{:edn :css} gp-config/mldoc-support-formats) format)))
+                       files)
+        support-files (sort-by :file/path support-files)
+        {journals true non-journals false} (group-by (fn [file] (string/includes? (:file/path file) "journals/")) support-files)
+        {built-in true others false} (group-by (fn [file]
+                                                 (or (string/includes? (:file/path file) "contents.")
+                                                     (string/includes? (:file/path file) ".edn")
+                                                     (string/includes? (:file/path file) "custom.css"))) non-journals)]
+    (concat (reverse journals) built-in others)))

+ 9 - 3
src/main/logseq/graph_parser/block.cljc → deps/graph-parser/src/logseq/graph_parser/block.cljc

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.block
+(ns logseq.graph-parser.block
   ;; Disable clj linters since we don't support clj
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})
                                         :unresolved-symbol {:level :off}}}})
@@ -174,8 +174,13 @@
                                            k)
                                            k)
                                        v (if (coll? v)
                                        v (if (coll? v)
                                            (remove string/blank? v)
                                            (remove string/blank? v)
-                                           (if (string/blank? v)
+                                           (cond
+                                             (string/blank? v)
                                              nil
                                              nil
+                                             (and (= (keyword k) :file-path)
+                                                  (string/starts-with? v "file:"))
+                                             v
+                                             :else
                                              (text/parse-property format k v user-config)))
                                              (text/parse-property format k v user-config)))
                                        k (keyword k)
                                        k (keyword k)
                                        v (if (and
                                        v (if (and
@@ -426,7 +431,8 @@
   (or (when-let [custom-id (or (get-in properties [:properties :custom-id])
   (or (when-let [custom-id (or (get-in properties [:properties :custom-id])
                                (get-in properties [:properties :custom_id])
                                (get-in properties [:properties :custom_id])
                                (get-in properties [:properties :id]))]
                                (get-in properties [:properties :id]))]
-        (let [custom-id (and (string? custom-id) (string/trim custom-id))]
+        ;; guard against non-string custom-ids
+        (when-let [custom-id (and (string? custom-id) (string/trim custom-id))]
           (some-> custom-id parse-uuid)))
           (some-> custom-id parse-uuid)))
       (d/squuid)))
       (d/squuid)))
 
 

+ 68 - 0
deps/graph-parser/src/logseq/graph_parser/cli.cljs

@@ -0,0 +1,68 @@
+(ns logseq.graph-parser.cli
+  "Primary ns to parse graphs with node.js based CLIs"
+  (:require ["fs" :as fs]
+            ["child_process" :as child-process]
+            [clojure.edn :as edn]
+            [clojure.string :as string]
+            [logseq.graph-parser :as graph-parser]
+            [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.db :as gp-db]))
+
+(defn slurp
+  "Return file contents like clojure.core/slurp"
+  [file]
+  (str (fs/readFileSync file)))
+
+(defn sh
+  "Run shell cmd synchronously and print to inherited streams by default. Aims
+    to be similar to babashka.tasks/shell
+TODO: Fail fast when process exits 1"
+  [cmd opts]
+  (child-process/spawnSync (first cmd)
+                           (clj->js (rest cmd))
+                           (clj->js (merge {:stdio "inherit"} opts))))
+
+(defn build-graph-files
+  "Given a git graph directory, returns allowed file paths and their contents in
+  preparation for parsing"
+  [dir]
+  (let [files (->> (str (.-stdout (sh ["git" "ls-files"]
+                                      {:cwd dir :stdio nil})))
+                   string/split-lines
+                   (map #(hash-map :file/path (str dir "/" %)))
+                   graph-parser/filter-files)]
+    (mapv #(assoc % :file/content (slurp (:file/path %))) files)))
+
+(defn- read-config
+  "Commandline version of frontend.handler.common/read-config without graceful
+  handling of broken config. Config is assumed to be at $dir/logseq/config.edn "
+  [dir]
+  (let [config-file (str dir "/" gp-config/app-name "/config.edn")]
+    (if (fs/existsSync config-file)
+     (-> config-file fs/readFileSync str edn/read-string)
+     {})))
+
+(defn- parse-files
+  [conn files {:keys [config] :as options}]
+  (let [extract-options (merge {:date-formatter (gp-config/get-date-formatter config)}
+                               (select-keys options [:verbose]))]
+    (doseq [{:file/keys [path content]} files]
+      (graph-parser/parse-file conn path content {:extract-options extract-options}))))
+
+(defn parse-graph
+  "Parses a given graph directory and returns a datascript connection and all
+  files that were processed. The directory is parsed as if it were a new graph
+  as it can't assume that the metadata in logseq/ is up to date. Directory is
+  assumed to be using git. This fn takes the following options:
+* :verbose - When enabled prints more information during parsing. Defaults to true
+* :files - Specific files to parse instead of parsing the whole directory"
+  ([dir]
+   (parse-graph dir {}))
+  ([dir options]
+   (let [files (or (:files options) (build-graph-files dir))
+         conn (gp-db/start-conn)
+         config (read-config dir)]
+     (when-not (:files options) (println "Parsing" (count files) "files..."))
+     (parse-files conn files (merge options {:config config}))
+     {:conn conn
+      :files (map :file/path files)})))

+ 60 - 0
deps/graph-parser/src/logseq/graph_parser/config.cljs

@@ -0,0 +1,60 @@
+(ns logseq.graph-parser.config
+  "Config that is shared between graph-parser and rest of app"
+  (:require [logseq.graph-parser.util :as gp-util]
+            [clojure.set :as set]
+            [clojure.string :as string]))
+
+(def app-name
+  "Copy of frontend.config/app-name. Too small to couple to main app"
+  "logseq")
+
+(defonce local-assets-dir "assets")
+
+(defn local-asset?
+  [s]
+  (gp-util/safe-re-find (re-pattern (str "^[./]*" local-assets-dir)) s))
+
+(defonce default-draw-directory "draws")
+
+(defn draw?
+  [path]
+  (string/starts-with? path default-draw-directory))
+
+;; TODO: rename
+(defonce mldoc-support-formats
+  #{:org :markdown :md})
+
+(defn mldoc-support?
+  [format]
+  (contains? mldoc-support-formats (keyword format)))
+
+(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
+    :excalidraw})
+
+(defn img-formats
+  []
+  #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})
+
+(defn supported-formats
+  []
+  (set/union (text-formats)
+             (img-formats)))
+
+(defn get-date-formatter
+  [config]
+  (or
+   (:journal/page-title-format config)
+   ;; for compatibility
+   (:date-formatter config)
+   "MMM do, yyyy"))
+
+(defn get-block-pattern
+  [format]
+  (let [format' (keyword format)]
+    (case format'
+      :org
+      "*"
+
+      "-")))

+ 2 - 2
src/main/logseq/graph_parser/date_time_util.cljs → deps/graph-parser/src/logseq/graph_parser/date_time_util.cljs

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.date-time-util
+(ns logseq.graph-parser.date-time-util
   "cljs-time util fns for graph-parser"
   "cljs-time util fns for graph-parser"
   (:require [cljs-time.coerce :as tc]
   (:require [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [cljs-time.core :as t]
@@ -25,7 +25,7 @@
                           (fn [formatter]
                           (fn [formatter]
                             (try
                             (try
                               (tf/parse (tf/formatter formatter) (gp-util/capitalize-all journal-title))
                               (tf/parse (tf/formatter formatter) (gp-util/capitalize-all journal-title))
-                              (catch js/Error _e
+                              (catch :default _e
                                 nil)))
                                 nil)))
                           formatters)
                           formatters)
                          (filter some?)
                          (filter some?)

+ 15 - 0
deps/graph-parser/src/logseq/graph_parser/db.cljs

@@ -0,0 +1,15 @@
+(ns logseq.graph-parser.db
+  (:require [logseq.graph-parser.db.default :as default-db]
+            [logseq.graph-parser.db.schema :as db-schema]
+            [datascript.core :as d]))
+
+(defn start-conn
+  "Create datascript conn with schema and default data"
+  []
+  (let [db-conn (d/create-conn db-schema/schema)]
+    (d/transact! db-conn [{:schema/version db-schema/version}
+                          {:block/name "card"
+                           :block/original-name "card"
+                           :block/uuid (d/squuid)}])
+    (d/transact! db-conn default-db/built-in-pages)
+    db-conn))

+ 1 - 1
src/main/frontend/db/default.cljs → deps/graph-parser/src/logseq/graph_parser/db/default.cljs

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible frontend.db.default
+(ns logseq.graph-parser.db.default
   (:require [clojure.string :as string]))
   (:require [clojure.string :as string]))
 
 
 (defonce built-in-pages-names
 (defonce built-in-pages-names

+ 1 - 1
src/main/frontend/db_schema.cljs → deps/graph-parser/src/logseq/graph_parser/db/schema.cljs

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible frontend.db-schema)
+(ns logseq.graph-parser.db.schema)
 
 
 (defonce version 1)
 (defonce version 1)
 (defonce ast-version 1)
 (defonce ast-version 1)

+ 60 - 53
src/main/logseq/graph_parser/extract.cljc → deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.extract
+(ns logseq.graph-parser.extract
   ;; Disable clj linters since we don't support clj
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})
                                         :unresolved-symbol {:level :off}}}})
@@ -39,12 +39,57 @@
               (or first-block-name file-name)
               (or first-block-name file-name)
               (or file-name first-block-name)))))))
               (or file-name first-block-name)))))))
 
 
+(defn- build-page-entity
+  [properties file page-name page ref-tags {:keys [date-formatter db]}]
+  (let [alias (:alias properties)
+        alias (if (string? alias) [alias] alias)
+        aliases (and alias
+                     (seq (remove #(or (= page-name (gp-util/page-name-sanity-lc %))
+                                       (string/blank? %)) ;; disable blank alias
+                                  alias)))
+        aliases (->>
+                 (map
+                  (fn [alias]
+                    (let [page-name (gp-util/page-name-sanity-lc alias)
+                          aliases (distinct
+                                   (conj
+                                    (remove #{alias} aliases)
+                                    page))
+                          aliases (when (seq aliases)
+                                    (map
+                                     (fn [alias]
+                                       {:block/name (gp-util/page-name-sanity-lc alias)})
+                                     aliases))]
+                      (if (seq aliases)
+                        {:block/name page-name
+                         :block/alias aliases}
+                        {:block/name page-name})))
+                  aliases)
+                 (remove nil?))]
+    (cond->
+     (gp-util/remove-nils
+      (assoc
+       (gp-block/page-name->map page false db true date-formatter)
+       :block/file {:file/path (gp-util/path-normalize file)}))
+     (seq properties)
+     (assoc :block/properties properties)
+
+     (seq aliases)
+     (assoc :block/alias aliases)
+
+     (:tags properties)
+     (assoc :block/tags (let [tags (:tags properties)
+                              tags (if (string? tags) [tags] tags)
+                              tags (remove string/blank? tags)]
+                          (swap! ref-tags set/union (set tags))
+                          (map (fn [tag] {:block/name (gp-util/page-name-sanity-lc tag)
+                                          :block/original-name tag})
+                               tags))))))
 
 
 ;; TODO: performance improvement
 ;; TODO: performance improvement
 (defn- extract-pages-and-blocks
 (defn- extract-pages-and-blocks
   [format ast properties file content {:keys [date-formatter page-name-order db] :as options}]
   [format ast properties file content {:keys [date-formatter page-name-order db] :as options}]
   (try
   (try
-    #_:clj-kondo/ignore ;;clj-kondo bug
     (let [page (get-page-name file ast page-name-order)
     (let [page (get-page-name file ast page-name-order)
           [_original-page-name page-name _journal-day] (gp-block/convert-page-if-journal page date-formatter)
           [_original-page-name page-name _journal-day] (gp-block/convert-page-if-journal page date-formatter)
           blocks (->> (gp-block/extract-blocks ast content false format (dissoc options :page-name-order))
           blocks (->> (gp-block/extract-blocks ast content false format (dissoc options :page-name-order))
@@ -65,50 +110,7 @@
                                      :block/refs block-ref-pages
                                      :block/refs block-ref-pages
                                      :block/path-refs block-path-ref-pages))))
                                      :block/path-refs block-path-ref-pages))))
                       blocks)
                       blocks)
-          page-entity (let [alias (:alias properties)
-                            alias (if (string? alias) [alias] alias)
-                            aliases (and alias
-                                         (seq (remove #(or (= page-name (gp-util/page-name-sanity-lc %))
-                                                           (string/blank? %)) ;; disable blank alias
-                                                      alias)))
-                            aliases (->>
-                                     (map
-                                      (fn [alias]
-                                        (let [page-name (gp-util/page-name-sanity-lc alias)
-                                              aliases (distinct
-                                                       (conj
-                                                        (remove #{alias} aliases)
-                                                        page))
-                                              aliases (when (seq aliases)
-                                                        (map
-                                                         (fn [alias]
-                                                           {:block/name (gp-util/page-name-sanity-lc alias)})
-                                                         aliases))]
-                                          (if (seq aliases)
-                                            {:block/name page-name
-                                             :block/alias aliases}
-                                            {:block/name page-name})))
-                                      aliases)
-                                     (remove nil?))]
-                        (cond->
-                         (gp-util/remove-nils
-                          (assoc
-                           (gp-block/page-name->map page false db true date-formatter)
-                           :block/file {:file/path (gp-util/path-normalize file)}))
-                         (seq properties)
-                         (assoc :block/properties properties)
-
-                         (seq aliases)
-                         (assoc :block/alias aliases)
-
-                         (:tags properties)
-                         (assoc :block/tags (let [tags (:tags properties)
-                                                  tags (if (string? tags) [tags] tags)
-                                                  tags (remove string/blank? tags)]
-                                              (swap! ref-tags set/union (set tags))
-                                              (map (fn [tag] {:block/name (gp-util/page-name-sanity-lc tag)
-                                                              :block/original-name tag})
-                                                   tags)))))
+          page-entity (build-page-entity properties file page-name page ref-tags options)
           namespace-pages (let [page (:block/original-name page-entity)]
           namespace-pages (let [page (:block/original-name page-entity)]
                             (when (text/namespace-page? page)
                             (when (text/namespace-page? page)
                               (->> (gp-util/split-namespace-pages page)
                               (->> (gp-util/split-namespace-pages page)
@@ -137,21 +139,23 @@
       (log/error :exception e))))
       (log/error :exception e))))
 
 
 (defn extract-blocks-pages
 (defn extract-blocks-pages
-  [file content {:keys [user-config] :as options}]
+  [file content {:keys [user-config verbose] :or {verbose true} :as options}]
   (if (string/blank? content)
   (if (string/blank? content)
     []
     []
     (let [format (gp-util/get-format file)
     (let [format (gp-util/get-format file)
-          _ (println "Parsing start: " file)
+          _ (when verbose (println "Parsing start: " file))
           ast (gp-mldoc/->edn content (gp-mldoc/default-config format
           ast (gp-mldoc/->edn content (gp-mldoc/default-config format
                                                          ;; {:parse_outline_only? true}
                                                          ;; {:parse_outline_only? true}
                                                          )
                                                          )
                            user-config)]
                            user-config)]
-      (println "Parsing finished : " file)
+      (when verbose (println "Parsing finished: " file))
       (let [first-block (ffirst ast)
       (let [first-block (ffirst ast)
             properties (let [properties (and (gp-property/properties-ast? first-block)
             properties (let [properties (and (gp-property/properties-ast? first-block)
                                              (->> (last first-block)
                                              (->> (last first-block)
                                                   (map (fn [[x y]]
                                                   (map (fn [[x y]]
-                                                         [x (if (string? y)
+                                                         [x (if (and (string? y)
+                                                                     (not (and (= (keyword x) :file-path)
+                                                                               (string/starts-with? y "file:"))))
                                                               (text/parse-property format x y user-config)
                                                               (text/parse-property format x y user-config)
                                                               y)]))
                                                               y)]))
                                                   (into {})
                                                   (into {})
@@ -184,6 +188,9 @@
          (map (partial apply merge))
          (map (partial apply merge))
          (with-block-uuid))))
          (with-block-uuid))))
 
 
-(defn extract-all-block-refs
-  [content]
-  (map second (re-seq #"\(\(([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})\)\)" content)))
+#?(:org.babashka/nbb
+   (alter-var-root #'gp-mldoc/parse-property (constantly text/parse-property))
+   :default
+   ;; TODO: Properly fix this circular dependency:
+   ;; mldoc/->edn > text/parse-property > mldoc/link? ->mldoc/inline->edn + mldoc/default-config
+   (set! gp-mldoc/parse-property text/parse-property))

+ 0 - 0
src/main/logseq/graph_parser/log.cljs → deps/graph-parser/src/logseq/graph_parser/log.cljs


+ 1 - 1
src/main/logseq/graph_parser/mldoc.cljc → deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.mldoc
+(ns logseq.graph-parser.mldoc
   ;; Disable clj linters since we don't support clj
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})
                                         :unresolved-symbol {:level :off}}}})

+ 2 - 1
src/main/logseq/graph_parser/property.cljs → deps/graph-parser/src/logseq/graph_parser/property.cljs

@@ -2,7 +2,8 @@
   "Property fns needed by graph-parser"
   "Property fns needed by graph-parser"
   (:require [logseq.graph-parser.util :as gp-util]
   (:require [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]
             [clojure.string :as string]
-            [goog.string :as gstring]))
+            [goog.string :as gstring]
+            [goog.string.format]))
 
 
 (defn properties-ast?
 (defn properties-ast?
   [block]
   [block]

+ 160 - 0
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -0,0 +1,160 @@
+(ns logseq.graph-parser.test.docs-graph-helper
+  "Helper fns for setting up and running tests against docs graph"
+  (:require ["fs" :as fs]
+            ["child_process" :as child-process]
+            [cljs.test :refer [is testing]]
+            [clojure.string :as string]
+            [logseq.graph-parser.config :as gp-config]
+            [datascript.core :as d]))
+
+;; Helper fns for test setup
+;; =========================
+(defn- sh
+  "Run shell cmd synchronously and print to inherited streams by default. Aims
+    to be similar to babashka.tasks/shell"
+  [cmd opts]
+  (child-process/spawnSync (first cmd)
+                           (clj->js (rest cmd))
+                           (clj->js (merge {:stdio "inherit"} opts))))
+
+(defn clone-docs-repo-if-not-exists
+  [dir]
+  (when-not (.existsSync fs dir)
+    (sh ["git" "clone" "--depth" "1" "-b" "v0.6.7" "-c" "advice.detachedHead=false"
+         "https://github.com/logseq/docs" dir] {})))
+
+
+;; Fns for common test assertions
+;; ==============================
+(defn- get-top-block-properties
+  [db]
+  (->> (d/q '[:find (pull ?b [*])
+              :where
+              [?b :block/properties]
+              [(missing? $ ?b :block/name)]]
+            db)
+       (map first)
+       (map (fn [m] (zipmap (keys (:block/properties m)) (repeat 1))))
+       (apply merge-with +)
+       (filter #(>= (val %) 5))
+       (into {})))
+
+(defn- get-all-page-properties
+  [db]
+  (->> (d/q '[:find (pull ?b [*])
+              :where
+              [?b :block/properties]
+              [?b :block/name]]
+            db)
+       (map first)
+       (map (fn [m] (zipmap (keys (:block/properties m)) (repeat 1))))
+       (apply merge-with +)
+       (into {})))
+
+(defn- get-block-format-counts
+  [db]
+  (->> (d/q '[:find (pull ?b [*]) :where [?b :block/format]] db)
+       (map first)
+       (group-by :block/format)
+       (map (fn [[k v]] [k (count v)]))
+       (into {})))
+
+(defn- query-assertions
+  [db files]
+  (testing "Query based stats"
+    (is (= (->> files
+                ;; logseq files aren't saved under :block/file
+                (remove #(string/includes? % (str "/" gp-config/app-name "/")))
+                set)
+           (->> (d/q '[:find (pull ?b [* {:block/file [:file/path]}])
+                       :where [?b :block/name] [?b :block/file]]
+                     db)
+                (map (comp #(get-in % [:block/file :file/path]) first))
+                set))
+        "Files on disk should equal ones in db")
+
+    (is (= (count (filter #(re-find #"journals/" %) files))
+           (->> (d/q '[:find (count ?b)
+                       :where
+                       [?b :block/journal? true]
+                       [?b :block/name]
+                       [?b :block/file]]
+                     db)
+                ffirst))
+        "Journal page count on disk equals count in db")
+
+    (is (= {"CANCELED" 2 "DONE" 6 "LATER" 4 "NOW" 5}
+           (->> (d/q '[:find (pull ?b [*]) :where [?b :block/marker] ]
+                     db)
+                (map first)
+                (group-by :block/marker)
+                (map (fn [[k v]] [k (count v)]))
+                (into {})))
+        "Task marker counts")
+
+    (is (= {:markdown 3143 :org 460}
+           (get-block-format-counts db))
+        "Block format counts")
+
+    (is (= {:title 98 :id 98
+            :updated-at 47 :created-at 47
+            :card-last-score 6 :card-repeats 6 :card-next-schedule 6
+            :card-last-interval 6 :card-ease-factor 6 :card-last-reviewed 6
+            :alias 6}
+           (get-top-block-properties db))
+        "Counts for top block properties")
+
+    (is (= {:title 98
+            :alias 6
+            :tags 2 :permalink 2
+            :name 1 :type 1 :related 1 :sample 1 :click 1 :id 1 :example 1}
+           (get-all-page-properties db))
+        "Counts for all page properties")
+
+    (is (= {:block/scheduled 2
+            :block/priority 4
+            :block/deadline 1
+            :block/collapsed? 22
+            :block/heading-level 60
+            :block/repeated? 1}
+           (->> [:block/scheduled :block/priority :block/deadline :block/collapsed?
+                 :block/heading-level :block/repeated?]
+                (map (fn [attr]
+                       [attr
+                        (ffirst (d/q [:find (list 'count '?b) :where ['?b attr]]
+                                     db))]))
+                (into {})))
+        "Counts for blocks with common block attributes")
+
+    (is (= #{"term" "setting" "book" "Templates" "Query" "Query/table" "page"}
+           (->> (d/q '[:find (pull ?n [*]) :where [?b :block/namespace ?n]] db)
+                (map (comp :block/original-name first))
+                set))
+        "Has correct namespaces")))
+
+(defn docs-graph-assertions
+  "These are common assertions that should pass in both graph-parser and main
+  logseq app. It is important to run these in both contexts to ensure that the
+  functionality in frontend.handler.repo and logseq.graph-parser remain the
+  same"
+  [db files]
+  ;; Counts assertions help check for no major regressions. These counts should
+  ;; only increase over time as the docs graph rarely has deletions
+  (testing "Counts"
+    (is (= 211 (count files)) "Correct file count")
+    (is (= 40943 (count (d/datoms db :eavt))) "Correct datoms count")
+
+    (is (= 3600
+           (ffirst
+            (d/q '[:find (count ?b)
+                   :where [?b :block/path-refs ?bp] [?bp :block/name]] db)))
+        "Correct referenced blocks count")
+    (is (= 21
+           (ffirst
+            (d/q '[:find (count ?b)
+                   :where [?b :block/content ?content]
+                   [(clojure.string/includes? ?content "+BEGIN_QUERY")]]
+                 db)))
+        "Advanced query count"))
+
+  (query-assertions db files))

+ 1 - 119
src/main/logseq/graph_parser/text.cljs → deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.text
+(ns logseq.graph-parser.text
   (:require ["path" :as path]
   (:require ["path" :as path]
             [goog.string :as gstring]
             [goog.string :as gstring]
             [clojure.string :as string]
             [clojure.string :as string]
@@ -51,8 +51,6 @@
 
 
 (def page-ref-re-without-nested #"\[\[([^\[\]]+)\]\]")
 (def page-ref-re-without-nested #"\[\[([^\[\]]+)\]\]")
 
 
-(defonce between-re #"\(between ([^\)]+)\)")
-
 (defn page-ref-un-brackets!
 (defn page-ref-un-brackets!
   [s]
   [s]
   (or (get-page-name s) s))
   (or (get-page-name s) s))
@@ -122,18 +120,6 @@
   [s]
   [s]
   (string/split s #"(\"[^\"]*\")"))
   (string/split s #"(\"[^\"]*\")"))
 
 
-(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\S+)?$")
-(def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
-(def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
-(def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")
-
-(defn get-matched-video
-  [url]
-  (or (re-find youtube-regex url)
-      (re-find loom-regex url)
-      (re-find vimeo-regex url)
-      (re-find bilibili-regex url)))
-
 (def markdown-link #"\[([^\[]+)\](\(.*\))")
 (def markdown-link #"\[([^\[]+)\](\(.*\))")
 
 
 (defn split-page-refs-without-brackets
 (defn split-page-refs-without-brackets
@@ -228,16 +214,6 @@
        :else
        :else
        (remove-level-space-aux! text block-pattern space? trim-left?)))))
        (remove-level-space-aux! text block-pattern space? trim-left?)))))
 
 
-(defn build-data-value
-  [col]
-  (let [items (map (fn [item] (str "\"" item "\"")) col)]
-    (gstring/format "[%s]"
-                 (string/join ", " items))))
-
-(defn media-link?
-  [media-formats s]
-  (some (fn [fmt] (gp-util/safe-re-find (re-pattern (str "(?i)\\." fmt "(?:\\?([^#]*))?(?:#(.*))?$")) s)) media-formats))
-
 (defn namespace-page?
 (defn namespace-page?
   [p]
   [p]
   (and (string? p)
   (and (string? p)
@@ -246,100 +222,6 @@
        (not (string/starts-with? p "./"))
        (not (string/starts-with? p "./"))
        (not (gp-util/url? p))))
        (not (gp-util/url? p))))
 
 
-(defn add-timestamp
-  [content key value]
-  (let [new-line (str (string/upper-case key) ": " value)
-        lines (string/split-lines content)
-        new-lines (map (fn [line]
-                         (string/trim
-                          (if (string/starts-with? (string/lower-case line) key)
-                            new-line
-                            line)))
-                    lines)
-        new-lines (if (not= (map string/trim lines) new-lines)
-                    new-lines
-                    (cons (first new-lines) ;; title
-                          (cons
-                           new-line
-                           (rest new-lines))))]
-    (string/join "\n" new-lines)))
-
-(defn remove-timestamp
-  [content key]
-  (let [lines (string/split-lines content)
-        new-lines (filter (fn [line]
-                            (not (string/starts-with? (string/lower-case line) key)))
-                          lines)]
-    (string/join "\n" new-lines)))
-
-(defn get-current-line-by-pos
-  [s pos]
-  (let [lines (string/split-lines s)
-        result (reduce (fn [acc line]
-                         (let [new-pos (+ acc (count line))]
-                           (if (>= new-pos pos)
-                             (reduced line)
-                             (inc new-pos)))) 0 lines)]
-    (when (string? result)
-      result)))
-
-(defn get-string-all-indexes
-  "Get all indexes of `value` in the string `s`."
-  [s value]
-  (loop [acc []
-         i 0]
-    (if-let [i (string/index-of s value i)]
-      (recur (conj acc i) (+ i (count value)))
-      acc)))
-
-(defn surround-by?
-  "`pos` must be surrounded by `before` and `and` in string `value`, e.g. ((|))"
-  [value pos before end]
-  (let [start-pos (if (= :start before) 0 (- pos (count before)))
-        end-pos (if (= :end end) (count value) (+ pos (count end)))]
-    (when (>= (count value) end-pos)
-      (= (cond
-           (and (= :end end) (= :start before))
-           ""
-
-           (= :end end)
-           before
-
-           (= :start before)
-           end
-
-           :else
-           (str before end))
-         (subs value start-pos end-pos)))))
-
-(defn wrapped-by?
-  "`pos` must be wrapped by `before` and `and` in string `value`, e.g. ((a|b))"
-  [value pos before end]
-  (let [before-matches (->> (get-string-all-indexes value before)
-                            (map (fn [i] [i :before])))
-        end-matches (->> (get-string-all-indexes value end)
-                         (map (fn [i] [i :end])))
-        indexes (sort-by first (concat before-matches end-matches [[pos :between]]))
-        ks (map second indexes)
-        q [:before :between :end]]
-    (true?
-     (reduce (fn [acc k]
-               (if (= q (conj acc k))
-                 (reduced true)
-                 (vec (take-last 2 (conj acc k)))))
-             []
-             ks))))
-
-(defn get-graph-name-from-path
-  [path]
-  (when (string? path)
-    (let [parts (->> (string/split path #"/")
-                     (take-last 2))]
-      (-> (if (not= (first parts) "0")
-            (string/join "/" parts)
-            (last parts))
-          js/decodeURI))))
-
 (defonce non-parsing-properties
 (defonce non-parsing-properties
   (atom #{"background-color" "background_color"}))
   (atom #{"background-color" "background_color"}))
 
 

+ 1 - 6
src/main/logseq/graph_parser/utf8.cljs → deps/graph-parser/src/logseq/graph_parser/utf8.cljs

@@ -1,5 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.utf8
-  (:require [goog.object :as gobj]))
+(ns logseq.graph-parser.utf8)
 
 
 (defonce encoder
 (defonce encoder
   (js/TextEncoder. "utf-8"))
   (js/TextEncoder. "utf-8"))
@@ -22,7 +21,3 @@
    (if end
    (if end
      (decode (.subarray arr start end))
      (decode (.subarray arr start end))
      (decode (.subarray arr start)))))
      (decode (.subarray arr start)))))
-
-(defn length
-  [arr]
-  (gobj/get arr "length"))

+ 2 - 5
src/main/logseq/graph_parser/util.cljs → deps/graph-parser/src/logseq/graph_parser/util.cljs

@@ -1,4 +1,4 @@
-(ns ^:nbb-compatible logseq.graph-parser.util
+(ns logseq.graph-parser.util
   "Util fns shared between graph-parser and rest of app. Util fns only rely on
   "Util fns shared between graph-parser and rest of app. Util fns only rely on
   clojure standard libraries."
   clojure standard libraries."
   (:require [clojure.walk :as walk]
   (:require [clojure.walk :as walk]
@@ -7,9 +7,6 @@
 (defn safe-re-find
 (defn safe-re-find
   "Copy of frontend.util/safe-re-find. Too basic to couple to main app"
   "Copy of frontend.util/safe-re-find. Too basic to couple to main app"
   [pattern s]
   [pattern s]
-  (when-not (string? s)
-    ;; TODO: sentry
-    (js/console.trace))
   (when (string? s)
   (when (string? s)
     (re-find pattern s)))
     (re-find pattern s)))
 
 
@@ -65,7 +62,7 @@
        (try
        (try
          (js/URL. s)
          (js/URL. s)
          true
          true
-         (catch js/Error _e
+         (catch :default _e
            false))))
            false))))
 
 
 (defn json->clj
 (defn json->clj

+ 2 - 1
src/test/logseq/graph_parser/block_test.cljs → deps/graph-parser/test/logseq/graph_parser/block_test.cljs

@@ -21,7 +21,8 @@
     [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
     [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
     [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
     [["foo" "[[bar]], [[nested [[baz]]]]"]] {:foo #{"bar" "nested [[baz]]"}}
     [["foo" "bar, [[baz, test]]"]] {:foo #{"bar" "baz, test"}}
     [["foo" "bar, [[baz, test]]"]] {:foo #{"bar" "baz, test"}}
-    [["foo" "bar, [[baz, test, [[nested]]]]"]] {:foo #{"bar" "baz, test, [[nested]]"}})
+    [["foo" "bar, [[baz, test, [[nested]]]]"]] {:foo #{"bar" "baz, test, [[nested]]"}}
+    [["file-path" "file:///home/x, y.pdf"]] {:file-path "file:///home/x, y.pdf"})
 
 
   (are [x y] (= (vec (:page-refs (gp-block/extract-properties :markdown x {}))) y)
   (are [x y] (= (vec (:page-refs (gp-block/extract-properties :markdown x {}))) y)
     [["year" "1000"]] []
     [["year" "1000"]] []

+ 13 - 0
deps/graph-parser/test/logseq/graph_parser/cli_test.cljs

@@ -0,0 +1,13 @@
+(ns logseq.graph-parser.cli-test
+  (:require [cljs.test :refer [deftest]]
+            [logseq.graph-parser.cli :as gp-cli]
+            [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]))
+
+;; Integration test that test parsing a large graph like docs
+(deftest ^:integration parse-graph
+  (let [graph-dir "test/docs"
+        _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
+        {:keys [conn files]} (gp-cli/parse-graph graph-dir)
+        db @conn]
+
+    (docs-graph-helper/docs-graph-assertions db files)))

+ 17 - 32
src/test/logseq/graph_parser/extract_test.cljs → deps/graph-parser/test/logseq/graph_parser/extract_test.cljs

@@ -1,12 +1,11 @@
 (ns logseq.graph-parser.extract-test
 (ns logseq.graph-parser.extract-test
-  (:require [cljs.test :refer [async deftest is]]
+  (:require [cljs.test :refer [deftest is]]
             [logseq.graph-parser.extract :as extract]
             [logseq.graph-parser.extract :as extract]
-            [clojure.pprint :as pprint]
-            [promesa.core :as p]))
+            [clojure.pprint :as pprint]))
 
 
 (defn- extract
 (defn- extract
   [text]
   [text]
-  (p/let [result (extract/extract-blocks-pages "a.md" text {:block-pattern "-"})
+  (let [result (extract/extract-blocks-pages "a.md" text {:block-pattern "-"})
           result (last result)
           result (last result)
           lefts (map (juxt :block/parent :block/left) result)]
           lefts (map (juxt :block/parent :block/left) result)]
     (if (not= (count lefts) (count (distinct lefts)))
     (if (not= (count lefts) (count (distinct lefts)))
@@ -15,34 +14,24 @@
         (throw (js/Error. ":block/parent && :block/left conflicts")))
         (throw (js/Error. ":block/parent && :block/left conflicts")))
       (mapv :block/content result))))
       (mapv :block/content result))))
 
 
-(defn- async-test
-  [x y]
-  (async done
-         (p/then
-          (extract x)
-          (fn [v]
-            (is (= y v))
-            (done)))))
-
 (deftest test-extract-blocks-pages
 (deftest test-extract-blocks-pages
   []
   []
-  (async-test
-   "- a
+  (is (= ["a" "b" "c"]
+         (extract
+          "- a
   - b
   - b
-    - c"
-   ["a" "b" "c"])
+    - c")))
 
 
-  (async-test
-   "## hello
+  (is (= ["## hello" "world" "nice" "nice" "bingo" "world"]
+         (extract "## hello
     - world
     - world
       - nice
       - nice
         - nice
         - nice
       - bingo
       - bingo
-      - world"
-   ["## hello" "world" "nice" "nice" "bingo" "world"])
+      - world")))
 
 
-  (async-test
-   "# a
+  (is (= ["# a" "## b" "### c" "#### d" "### e" "f" "g" "h" "i" "j"]
+       (extract "# a
 ## b
 ## b
 ### c
 ### c
 #### d
 #### d
@@ -51,17 +40,13 @@
   - g
   - g
     - h
     - h
   - i
   - i
-- j"
-
-   ["# a" "## b" "### c" "#### d" "### e" "f" "g" "h" "i" "j"]))
+- j"))))
 
 
 (deftest test-regression-1902
 (deftest test-regression-1902
   []
   []
-  (async-test
-   "- line1
+  (is (= ["line1" "line2" "line3" "line4"]
+         (extract
+          "- line1
     - line2
     - line2
       - line3
       - line3
-     - line4"
-   ["line1" "line2" "line3" "line4"]))
-
-#_(cljs.test/run-tests)
+     - line4"))))

+ 7 - 6
src/test/logseq/graph_parser/mldoc_test.cljs → deps/graph-parser/test/logseq/graph_parser/mldoc_test.cljs

@@ -1,7 +1,8 @@
 (ns logseq.graph-parser.mldoc-test
 (ns logseq.graph-parser.mldoc-test
   (:require [logseq.graph-parser.mldoc :as gp-mldoc]
   (:require [logseq.graph-parser.mldoc :as gp-mldoc]
             [clojure.string :as string]
             [clojure.string :as string]
-            [frontend.test.docs-graph-helper :as docs-graph-helper]
+            [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
+            [logseq.graph-parser.cli :as gp-cli]
             [cljs.test :refer [testing deftest are is]]))
             [cljs.test :refer [testing deftest are is]]))
 
 
 (deftest test-link
 (deftest test-link
@@ -98,9 +99,9 @@
 : definition" md-config {})))))
 : definition" md-config {})))))
 
 
 (deftest ^:integration test->edn
 (deftest ^:integration test->edn
-  (let [graph-dir "src/test/docs"
+  (let [graph-dir "test/docs"
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
         _ (docs-graph-helper/clone-docs-repo-if-not-exists graph-dir)
-        files (docs-graph-helper/build-graph-files graph-dir)
+        files (gp-cli/build-graph-files graph-dir)
         asts-by-file (->> files
         asts-by-file (->> files
                           (map (fn [{:file/keys [path content]}]
                           (map (fn [{:file/keys [path content]}]
                                  (let [format (if (string/ends-with? path ".org")
                                  (let [format (if (string/ends-with? path ".org")
@@ -116,10 +117,10 @@
             "Drawer" 1,
             "Drawer" 1,
             "Example" 20,
             "Example" 20,
             "Footnote_Definition" 2,
             "Footnote_Definition" 2,
-            "Heading" 3493,
+            "Heading" 3496,
             "Hiccup" 15,
             "Hiccup" 15,
-            "List" 36,
-            "Paragraph" 411,
+            "List" 37,
+            "Paragraph" 417,
             "Properties" 104,
             "Properties" 104,
             "Property_Drawer" 188,
             "Property_Drawer" 188,
             "Quote" 9,
             "Quote" 9,

+ 4 - 8
src/test/logseq/graph_parser/nbb_test_runner.cljs → deps/graph-parser/test/logseq/graph_parser/nbb_test_runner.cljs

@@ -1,13 +1,12 @@
 (ns logseq.graph-parser.nbb-test-runner
 (ns logseq.graph-parser.nbb-test-runner
   "Nbb tests for graph-parser"
   "Nbb tests for graph-parser"
   (:require [cljs.test :as t]
   (:require [cljs.test :as t]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text-test]
             [logseq.graph-parser.text-test]
             [logseq.graph-parser.mldoc-test]
             [logseq.graph-parser.mldoc-test]
             [logseq.graph-parser.block-test]
             [logseq.graph-parser.block-test]
             [logseq.graph-parser.property-test]
             [logseq.graph-parser.property-test]
-            [logseq.graph-parser.extract-test]))
+            [logseq.graph-parser.extract-test]
+            [logseq.graph-parser.cli-test]))
 
 
 (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
 (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
   (when-not (cljs.test/successful? m)
   (when-not (cljs.test/successful? m)
@@ -15,12 +14,9 @@
 
 
 ;; run this function with: nbb-logseq -m logseq.test.nbb-test-runner/run-tests
 ;; run this function with: nbb-logseq -m logseq.test.nbb-test-runner/run-tests
 (defn run-tests []
 (defn run-tests []
-  ;; This hack is the same as the one in frontend.format. This has to be in an nbb only
-  ;; ns since alter-var-root doesn't exist in cljs and nbb doesn't support set! yet
-  #_:clj-kondo/ignore
-  (alter-var-root #'gp-mldoc/parse-property (constantly text/parse-property))
   (t/run-tests 'logseq.graph-parser.mldoc-test
   (t/run-tests 'logseq.graph-parser.mldoc-test
                'logseq.graph-parser.text-test
                'logseq.graph-parser.text-test
                'logseq.graph-parser.property-test
                'logseq.graph-parser.property-test
                'logseq.graph-parser.block-test
                'logseq.graph-parser.block-test
-               'logseq.graph-parser.extract-test))
+               'logseq.graph-parser.extract-test
+               'logseq.graph-parser.cli-test))

+ 0 - 0
src/test/logseq/graph_parser/property_test.cljs → deps/graph-parser/test/logseq/graph_parser/property_test.cljs


+ 0 - 30
src/test/logseq/graph_parser/text_test.cljs → deps/graph-parser/test/logseq/graph_parser/text_test.cljs

@@ -98,36 +98,6 @@
       "**foobar" "foobar"
       "**foobar" "foobar"
       "*********************foobar" "foobar")))
       "*********************foobar" "foobar")))
 
 
-(deftest test-add-timestamp
-  []
-  (are [x y] (= x y)
-    (text/add-timestamp "LATER hello world\nhello"
-                        "scheduled"
-                        "<2021-08-25 Wed>")
-    "LATER hello world\nSCHEDULED: <2021-08-25 Wed>\nhello"
-
-    (text/add-timestamp "LATER hello world "
-                        "scheduled"
-                        "<2021-08-25 Wed>")
-    "LATER hello world\nSCHEDULED: <2021-08-25 Wed>"
-
-    (text/add-timestamp "LATER hello world\nfoo:: bar\ntest"
-                        "scheduled"
-                        "<2021-08-25 Wed>")
-    "LATER hello world\nSCHEDULED: <2021-08-25 Wed>\nfoo:: bar\ntest"))
-
-(deftest get-string-all-indexes
-  []
-  (are [x y] (= x y)
-    (text/get-string-all-indexes "[[hello]] [[world]]" "[[")
-    [0 10]
-
-    (text/get-string-all-indexes "abc abc ab" "ab")
-    [0 4 8]
-
-    (text/get-string-all-indexes "a.c a.c ab" "a.")
-    [0 4]))
-
 (deftest test-parse-property
 (deftest test-parse-property
   (testing "parse-property"
   (testing "parse-property"
     (are [k v y] (= (text/parse-property k v {}) y)
     (are [k v y] (= (text/parse-property k v {}) y)

+ 411 - 0
deps/graph-parser/yarn.lock

@@ -0,0 +1,411 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@logseq/nbb-logseq@^0.5.103":
+  version "0.5.103"
+  resolved "https://registry.yarnpkg.com/@logseq/nbb-logseq/-/nbb-logseq-0.5.103.tgz#1084380cd54c92ca8cc94a8934cc777206e45cc0"
+  integrity sha512-V9UW0XrCaaadHUc6/Hp9wfGpQqkzqzoqnDGeSVZkWR6l3QwyqGi9mkhnhVcfTwAvxIfOgrfz93GcaeepV4pYNA==
+  dependencies:
+    import-meta-resolve "^1.1.1"
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==
+
+ansi-regex@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
+  integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
+builtins@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/builtins/-/builtins-4.1.0.tgz#1edd016dd91ce771a1ed6fc3b2b71fb918953250"
+  integrity sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==
+  dependencies:
+    semver "^7.0.0"
+
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+cliui@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+  integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+  dependencies:
+    string-width "^2.1.1"
+    strip-ansi "^4.0.0"
+    wrap-ansi "^2.0.0"
+
+code-point-at@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+  integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+cross-spawn@^6.0.0:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+end-of-stream@^1.1.0:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+execa@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+  integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+  dependencies:
+    cross-spawn "^6.0.0"
+    get-stream "^4.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+get-caller-file@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+  integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
+get-stream@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+  dependencies:
+    pump "^3.0.0"
+
+import-meta-resolve@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-1.1.1.tgz#244fd542fd1fae73550d4f8b3cde3bba1d7b2b18"
+  integrity sha512-JiTuIvVyPaUg11eTrNDx5bgQ/yMKMZffc7YSjvQeSMXy58DO2SQ8BtAf3xteZvmzvjYh14wnqNjL8XVeDy2o9A==
+  dependencies:
+    builtins "^4.0.0"
+
+invert-kv@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+  integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+is-fullwidth-code-point@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+  integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-stream@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+  integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+lcid@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+  integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+  dependencies:
+    invert-kv "^2.0.0"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+map-age-cleaner@^0.1.1:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+  integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+  dependencies:
+    p-defer "^1.0.0"
+
+mem@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
+  integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
+  dependencies:
+    map-age-cleaner "^0.1.1"
+    mimic-fn "^2.0.0"
+    p-is-promise "^2.0.0"
+
+mimic-fn@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mldoc@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.3.3.tgz#b7f39b48eb0ef3558619d3e3522265977bd78fe3"
+  integrity sha512-TzW06GBltdKxwWAxOvflPmIVedu6bzl9T4YoYqnDUyZ3kELFMllEgiYCh65PPW3xsRMA/5OcRQqqGZGiKEJEug==
+  dependencies:
+    yargs "^12.0.2"
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+  dependencies:
+    path-key "^2.0.0"
+
+number-is-nan@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+os-locale@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+  integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+  dependencies:
+    execa "^1.0.0"
+    lcid "^2.0.0"
+    mem "^4.0.0"
+
+p-defer@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+  integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
+  integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
+
+p-limit@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-key@^2.0.0, path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+  integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
+semver@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^7.0.0:
+  version "7.3.7"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+  integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
+  dependencies:
+    lru-cache "^6.0.0"
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+signal-exit@^3.0.0:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+  integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+string-width@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+  integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+wrap-ansi@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+  integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+"y18n@^3.2.1 || ^4.0.0":
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+  integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@^11.1.1:
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+  integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^12.0.2:
+  version "12.0.5"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+  integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+  dependencies:
+    cliui "^4.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^1.0.1"
+    os-locale "^3.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1 || ^4.0.0"
+    yargs-parser "^11.1.1"

+ 2 - 2
docs/contributing-to-translations.md

@@ -14,9 +14,9 @@ In order to run the commands in this doc, you will need to install
 ## Where to Contribute
 ## Where to Contribute
 
 
 Language translations are in two files,
 Language translations are in two files,
-[frontend/dicts.cljs](https://github.com/logseq/logseq/blob/master/src/main/frontend/dicts.cljs)
+[frontend/dicts.cljc](https://github.com/logseq/logseq/blob/master/src/main/frontend/dicts.cljc)
 and
 and
-[shortcut/dict.cljs](https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/dicts.cljs).
+[shortcut/dict.cljc](https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/dicts.cljc).
 
 
 ## Language Overview
 ## Language Overview
 
 

+ 7 - 1
docs/dev-practices.md

@@ -62,7 +62,7 @@ scripts/lint_rules.clj
 
 
 Namespaces have the metadata flag `^:nbb-compatible` indicate they are compatible with https://github.com/logseq/nbb-logseq. This compatibility is necessary in order for namespaces to be reused by the frontend and CLIs. To confirm these compatibilities, run:
 Namespaces have the metadata flag `^:nbb-compatible` indicate they are compatible with https://github.com/logseq/nbb-logseq. This compatibility is necessary in order for namespaces to be reused by the frontend and CLIs. To confirm these compatibilities, run:
 ```
 ```
-bb test:load-nbb-compatible-namespaces
+bb test:load-namespaces-with-nbb
 ```
 ```
 
 
 ## Testing
 ## Testing
@@ -135,3 +135,9 @@ Specs should go under `src/main/frontend/spec/` and be compatible with clojure
 and clojurescript. See `frontend.spec.storage` for an example. By following
 and clojurescript. See `frontend.spec.storage` for an example. By following
 these conventions, specs should also be usable by babashka. This is helpful as it
 these conventions, specs should also be usable by babashka. This is helpful as it
 allows for third party tools to be written with logseq's data model.
 allows for third party tools to be written with logseq's data model.
+
+## Development Tools
+
+There are some babashka tasks under `nbb:` which are useful for inspecting
+database changes in realtime. See [these
+docs](https://github.com/logseq/bb-tasks#logseqbb-tasksnbbwatch) for more info.

+ 1 - 1
docs/mobile.md

@@ -2,7 +2,7 @@
 * Android studio
 * Android studio
 * SDK 30
 * SDK 30
 * other sdk tools in Android studio preference setting https://capacitorjs.com/docs/getting-started/environment-setup
 * other sdk tools in Android studio preference setting https://capacitorjs.com/docs/getting-started/environment-setup
-* change the server url in capacitor.config.json with your local ip:3001 (run ifconfig to check)
+* change the server url in `capacitor.config.ts` with your local ip:3001 (run ifconfig to check)
 * run `yarn && yarn app-watch`
 * run `yarn && yarn app-watch`
 * in another console, run `npx cap open android`
 * in another console, run `npx cap open android`
 * create Android virtual device in Android studio
 * create Android virtual device in Android studio

+ 3 - 3
e2e-tests/sidebar.spec.ts

@@ -16,12 +16,12 @@ test('favorite item and recent item test', async ({ page }) => {
   await page.locator("text=Add to Favorites").click()
   await page.locator("text=Add to Favorites").click()
   // click from another page
   // click from another page
   const another_page_name = await createRandomPage(page)
   const another_page_name = await createRandomPage(page)
-  expect(await page.innerText(':nth-match(.favorite-item a, 1)')).toBe('◦' + fav_page_name)
+  expect(await page.innerText(':nth-match(.favorite-item a, 1)')).toBe(fav_page_name)
   await page.click(":nth-match(.favorite-item, 1)")
   await page.click(":nth-match(.favorite-item, 1)")
   expect(await page.innerText('.page-title .title')).toBe(fav_page_name)
   expect(await page.innerText('.page-title .title')).toBe(fav_page_name)
 
 
-  expect(await page.innerText(':nth-match(.recent-item a, 1)')).toBe('◦' + fav_page_name)
-  expect(await page.innerText(':nth-match(.recent-item a, 2)')).toBe('◦' + another_page_name)
+  expect(await page.innerText(':nth-match(.recent-item a, 1)')).toBe(fav_page_name)
+  expect(await page.innerText(':nth-match(.recent-item a, 2)')).toBe(another_page_name)
 
 
   // remove fav
   // remove fav
   await page.click('.ui__dropdown-trigger')
   await page.click('.ui__dropdown-trigger')

+ 6 - 6
ios/App/App.xcodeproj/project.pbxproj

@@ -474,7 +474,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
 				SDKROOT = iphoneos;
@@ -528,7 +528,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -550,7 +550,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.1;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -576,7 +576,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.1;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -601,7 +601,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.1;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -628,7 +628,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.10;
+				MARKETING_VERSION = 0.7.1;
 				MTL_FAST_MATH = YES;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 0
ios/App/Podfile

@@ -13,6 +13,7 @@ def capacitor_pods
   pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
   pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
   pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
   pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
   pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
   pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
+  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'

+ 4 - 2
package.json

@@ -63,7 +63,7 @@
         "cljs:debug": "clojure -M:cljs release app --debug",
         "cljs:debug": "clojure -M:cljs release app --debug",
         "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html",
         "cljs:report": "clojure -M:cljs run shadow.cljs.build-report app report.html",
         "cljs:build-electron": "clojure -A:cljs compile app electron",
         "cljs:build-electron": "clojure -A:cljs compile app electron",
-        "cljs:lint": "clojure -M:clj-kondo --parallel --lint src",
+        "cljs:lint": "clojure -M:clj-kondo --parallel --lint src --cache false",
         "tldraw:build": "cd tldraw && yarn build"
         "tldraw:build": "cd tldraw && yarn build"
     },
     },
     "dependencies": {
     "dependencies": {
@@ -73,6 +73,7 @@
         "@capacitor/clipboard": "^1.0.8",
         "@capacitor/clipboard": "^1.0.8",
         "@capacitor/core": "3.2.2",
         "@capacitor/core": "3.2.2",
         "@capacitor/filesystem": "1.0.6",
         "@capacitor/filesystem": "1.0.6",
+        "@capacitor/haptics": "^1.1.4",
         "@capacitor/ios": "3.2.2",
         "@capacitor/ios": "3.2.2",
         "@capacitor/keyboard": "^1.2.0",
         "@capacitor/keyboard": "^1.2.0",
         "@capacitor/share": "^1.1.2",
         "@capacitor/share": "^1.1.2",
@@ -85,6 +86,7 @@
         "@sentry/tracing": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
         "@tabler/icons": "1.54.0",
         "@tabler/icons": "1.54.0",
         "@tippyjs/react": "4.2.5",
         "@tippyjs/react": "4.2.5",
+        "bignumber.js": "^9.0.2",
         "capacitor-voice-recorder": "2.1.0",
         "capacitor-voice-recorder": "2.1.0",
         "chokidar": "3.5.1",
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
         "chrono-node": "2.2.4",
@@ -103,7 +105,7 @@
         "ignore": "5.1.8",
         "ignore": "5.1.8",
         "is-svg": "4.3.0",
         "is-svg": "4.3.0",
         "jszip": "3.5.0",
         "jszip": "3.5.0",
-        "mldoc": "1.3.3",
+        "mldoc": "1.4.0",
         "mobx": "^6.5.0",
         "mobx": "^6.5.0",
         "path": "0.12.7",
         "path": "0.12.7",
         "pixi-graph-fork": "0.2.0",
         "pixi-graph-fork": "0.2.0",

+ 3 - 3
resources/css/common.css

@@ -11,7 +11,7 @@
   --ls-border-radius-medium: 8px;
   --ls-border-radius-medium: 8px;
   --ls-headbar-height: 3rem;
   --ls-headbar-height: 3rem;
   --ls-headbar-inner-top-padding: 0px;
   --ls-headbar-inner-top-padding: 0px;
-  --ls-left-sidebar-width: 240px;
+  --ls-left-sidebar-width: 246px;
   --ls-left-sidebar-sm-width: 70%;
   --ls-left-sidebar-sm-width: 70%;
   --ls-left-sidebar-nav-btn-size: 38px;
   --ls-left-sidebar-nav-btn-size: 38px;
 }
 }
@@ -101,8 +101,8 @@ See: https://github.com/logseq/logseq/pull/4652. */
 html[data-theme='light'] {
 html[data-theme='light'] {
   --ls-primary-background-color: #ffffff;
   --ls-primary-background-color: #ffffff;
   --ls-secondary-background-color: #f7f7f7;
   --ls-secondary-background-color: #f7f7f7;
-  --ls-tertiary-background-color: #f1eee8;
-  --ls-quaternary-background-color: #e8e5de;
+  --ls-tertiary-background-color: #eaeaea;
+  --ls-quaternary-background-color: #dcdcdc;
   --ls-table-tr-even-background-color: #f7f7f7;
   --ls-table-tr-even-background-color: #f7f7f7;
   --ls-active-primary-color: rgb(0, 105, 182);
   --ls-active-primary-color: rgb(0, 105, 182);
   --ls-active-secondary-color: #00477c;
   --ls-active-secondary-color: #00477c;

+ 9 - 0
resources/css/shepherd.css

@@ -0,0 +1,9 @@
+.shepherd-button{background:#3288e6;border:0;border-radius:3px;color:hsla(0,0%,100%,.75);cursor:pointer;margin-right:.5rem;padding:.5rem 1.5rem;transition:all .5s ease}.shepherd-button:not(:disabled):hover{background:#196fcc;color:hsla(0,0%,100%,.75)}.shepherd-button.shepherd-button-secondary{background:#f1f2f3;color:rgba(0,0,0,.75)}.shepherd-button.shepherd-button-secondary:not(:disabled):hover{background:#d6d9db;color:rgba(0,0,0,.75)}.shepherd-button:disabled{cursor:not-allowed}
+.shepherd-footer{border-bottom-left-radius:5px;border-bottom-right-radius:5px;display:flex;justify-content:flex-end;padding:0 .75rem .75rem}.shepherd-footer .shepherd-button:last-child{margin-right:0}
+.shepherd-cancel-icon{background:transparent;border:none;color:hsla(0,0%,50%,.75);cursor:pointer;font-size:2em;font-weight:400;margin:0;padding:0;transition:color .5s ease}.shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon{color:hsla(0,0%,50%,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}
+.shepherd-title{color:rgba(0,0,0,.75);display:flex;flex:1 0 auto;font-size:1rem;font-weight:400;margin:0;padding:0}
+.shepherd-header{align-items:center;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:flex-end;line-height:2em;padding:.75rem .75rem 0}.shepherd-has-title .shepherd-content .shepherd-header{background:#e6e6e6;padding:1em}
+.shepherd-text{color:rgba(0,0,0,.75);font-size:1rem;line-height:1.3em;padding:.75em}.shepherd-text p{margin-top:0}.shepherd-text p:last-child{margin-bottom:0}
+.shepherd-content{border-radius:5px;outline:none;padding:0}
+.shepherd-element{background:#fff;border-radius:5px;box-shadow:0 1px 4px rgba(0,0,0,.2);max-width:400px;opacity:0;outline:none;transition:opacity .3s,visibility .3s;visibility:hidden;width:100%;z-index:9999}.shepherd-enabled.shepherd-element{opacity:1;visibility:visible}.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered){opacity:0;pointer-events:none;visibility:hidden}.shepherd-element,.shepherd-element *,.shepherd-element :after,.shepherd-element :before{box-sizing:border-box}.shepherd-arrow,.shepherd-arrow:before{height:16px;position:absolute;width:16px;z-index:-1}.shepherd-arrow:before{background:#fff;content:"";transform:rotate(45deg)}.shepherd-element[data-popper-placement^=top]>.shepherd-arrow{bottom:-8px}.shepherd-element[data-popper-placement^=bottom]>.shepherd-arrow{top:-8px}.shepherd-element[data-popper-placement^=left]>.shepherd-arrow{right:-8px}.shepherd-element[data-popper-placement^=right]>.shepherd-arrow{left:-8px}.shepherd-element.shepherd-centered>.shepherd-arrow{opacity:0}.shepherd-element.shepherd-has-title[data-popper-placement^=bottom]>.shepherd-arrow:before{background-color:#e6e6e6}.shepherd-target-click-disabled.shepherd-enabled.shepherd-target,.shepherd-target-click-disabled.shepherd-enabled.shepherd-target *{pointer-events:none}
+.shepherd-modal-overlay-container{height:0;left:0;opacity:0;overflow:hidden;pointer-events:none;position:fixed;top:0;transition:all .3s ease-out,height 0ms .3s,opacity .3s 0ms;width:100vw;z-index:9997}.shepherd-modal-overlay-container.shepherd-modal-is-visible{height:100vh;opacity:.5;transition:all .3s ease-out,height 0s 0s,opacity .3s 0s}.shepherd-modal-overlay-container.shepherd-modal-is-visible path{pointer-events:all}

+ 117 - 0
resources/js/shepherd.min.js

@@ -0,0 +1,117 @@
+/*! shepherd.js 9.1.0 */
+
+'use strict';(function(O,pa){"object"===typeof exports&&"undefined"!==typeof module?module.exports=pa():"function"===typeof define&&define.amd?define(pa):(O="undefined"!==typeof globalThis?globalThis:O||self,O.Shepherd=pa())})(this,function(){function O(a,b){return!1!==b.clone&&b.isMergeableObject(a)?da(Array.isArray(a)?[]:{},a,b):a}function pa(a,b,c){return a.concat(b).map(function(d){return O(d,c)})}function Db(a){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(a).filter(function(b){return a.propertyIsEnumerable(b)}):
+[]}function Ta(a){return Object.keys(a).concat(Db(a))}function Ua(a,b){try{return b in a}catch(c){return!1}}function Eb(a,b,c){var d={};c.isMergeableObject(a)&&Ta(a).forEach(function(e){d[e]=O(a[e],c)});Ta(b).forEach(function(e){if(!Ua(a,e)||Object.hasOwnProperty.call(a,e)&&Object.propertyIsEnumerable.call(a,e))if(Ua(a,e)&&c.isMergeableObject(b[e])){if(c.customMerge){var f=c.customMerge(e);f="function"===typeof f?f:da}else f=da;d[e]=f(a[e],b[e],c)}else d[e]=O(b[e],c)});return d}function da(a,b,c){c=
+c||{};c.arrayMerge=c.arrayMerge||pa;c.isMergeableObject=c.isMergeableObject||Fb;c.cloneUnlessOtherwiseSpecified=O;var d=Array.isArray(b),e=Array.isArray(a);return d!==e?O(b,c):d?c.arrayMerge(a,b,c):Eb(a,b,c)}function ea(a){return"function"===typeof a}function qa(a){return"string"===typeof a}function Va(a){let b=Object.getOwnPropertyNames(a.constructor.prototype);for(let c=0;c<b.length;c++){let d=b[c],e=a[d];"constructor"!==d&&"function"===typeof e&&(a[d]=e.bind(a))}return a}function Gb(a,b){return c=>
+{if(b.isOpen()){let d=b.el&&c.currentTarget===b.el;(void 0!==a&&c.currentTarget.matches(a)||d)&&b.tour.next()}}}function Hb(a){let {event:b,selector:c}=a.options.advanceOn||{};if(b){let d=Gb(c,a),e;try{e=document.querySelector(c)}catch(f){}if(void 0===c||e)e?(e.addEventListener(b,d),a.on("destroy",()=>e.removeEventListener(b,d))):(document.body.addEventListener(b,d,!0),a.on("destroy",()=>document.body.removeEventListener(b,d,!0)));else return console.error(`No element was found for the selector supplied to advanceOn: ${c}`)}else return console.error("advanceOn was defined, but no event name was passed.")}
+function M(a){return a?(a.nodeName||"").toLowerCase():null}function K(a){return null==a?window:"[object Window]"!==a.toString()?(a=a.ownerDocument)?a.defaultView||window:window:a}function fa(a){var b=K(a).Element;return a instanceof b||a instanceof Element}function F(a){var b=K(a).HTMLElement;return a instanceof b||a instanceof HTMLElement}function Ea(a){if("undefined"===typeof ShadowRoot)return!1;var b=K(a).ShadowRoot;return a instanceof b||a instanceof ShadowRoot}function N(a){return a.split("-")[0]}
+function ha(a,b){void 0===b&&(b=!1);var c=a.getBoundingClientRect(),d=1,e=1;F(a)&&b&&(b=a.offsetHeight,a=a.offsetWidth,0<a&&(d=ia(c.width)/a||1),0<b&&(e=ia(c.height)/b||1));return{width:c.width/d,height:c.height/e,top:c.top/e,right:c.right/d,bottom:c.bottom/e,left:c.left/d,x:c.left/d,y:c.top/e}}function Fa(a){var b=ha(a),c=a.offsetWidth,d=a.offsetHeight;1>=Math.abs(b.width-c)&&(c=b.width);1>=Math.abs(b.height-d)&&(d=b.height);return{x:a.offsetLeft,y:a.offsetTop,width:c,height:d}}function Wa(a,b){var c=
+b.getRootNode&&b.getRootNode();if(a.contains(b))return!0;if(c&&Ea(c)){do{if(b&&a.isSameNode(b))return!0;b=b.parentNode||b.host}while(b)}return!1}function P(a){return K(a).getComputedStyle(a)}function U(a){return((fa(a)?a.ownerDocument:a.document)||window.document).documentElement}function wa(a){return"html"===M(a)?a:a.assignedSlot||a.parentNode||(Ea(a)?a.host:null)||U(a)}function Xa(a){return F(a)&&"fixed"!==P(a).position?a.offsetParent:null}function ra(a){for(var b=K(a),c=Xa(a);c&&0<=["table","td",
+"th"].indexOf(M(c))&&"static"===P(c).position;)c=Xa(c);if(c&&("html"===M(c)||"body"===M(c)&&"static"===P(c).position))return b;if(!c)a:{c=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1===navigator.userAgent.indexOf("Trident")||!F(a)||"fixed"!==P(a).position)for(a=wa(a),Ea(a)&&(a=a.host);F(a)&&0>["html","body"].indexOf(M(a));){var d=P(a);if("none"!==d.transform||"none"!==d.perspective||"paint"===d.contain||-1!==["transform","perspective"].indexOf(d.willChange)||c&&"filter"===d.willChange||
+c&&d.filter&&"none"!==d.filter){c=a;break a}else a=a.parentNode}c=null}return c||b}function Ga(a){return 0<=["top","bottom"].indexOf(a)?"x":"y"}function Ya(a){return Object.assign({},{top:0,right:0,bottom:0,left:0},a)}function Za(a,b){return b.reduce(function(c,d){c[d]=a;return c},{})}function ja(a){return a.split("-")[1]}function $a(a){var b,c=a.popper,d=a.popperRect,e=a.placement,f=a.variation,g=a.offsets,l=a.position,m=a.gpuAcceleration,k=a.adaptive,p=a.roundOffsets,q=a.isFixed;a=g.x;a=void 0===
+a?0:a;var n=g.y,r=void 0===n?0:n;n="function"===typeof p?p({x:a,y:r}):{x:a,y:r};a=n.x;r=n.y;n=g.hasOwnProperty("x");g=g.hasOwnProperty("y");var x="left",h="top",t=window;if(k){var v=ra(c),A="clientHeight",u="clientWidth";v===K(c)&&(v=U(c),"static"!==P(v).position&&"absolute"===l&&(A="scrollHeight",u="scrollWidth"));if("top"===e||("left"===e||"right"===e)&&"end"===f)h="bottom",r-=(q&&v===t&&t.visualViewport?t.visualViewport.height:v[A])-d.height,r*=m?1:-1;if("left"===e||("top"===e||"bottom"===e)&&
+"end"===f)x="right",a-=(q&&v===t&&t.visualViewport?t.visualViewport.width:v[u])-d.width,a*=m?1:-1}c=Object.assign({position:l},k&&Ib);!0===p?(p=r,d=window.devicePixelRatio||1,a={x:ia(a*d)/d||0,y:ia(p*d)/d||0}):a={x:a,y:r};p=a;a=p.x;r=p.y;if(m){var w;return Object.assign({},c,(w={},w[h]=g?"0":"",w[x]=n?"0":"",w.transform=1>=(t.devicePixelRatio||1)?"translate("+a+"px, "+r+"px)":"translate3d("+a+"px, "+r+"px, 0)",w))}return Object.assign({},c,(b={},b[h]=g?r+"px":"",b[x]=n?a+"px":"",b.transform="",b))}
+function xa(a){return a.replace(/left|right|bottom|top/g,function(b){return Jb[b]})}function ab(a){return a.replace(/start|end/g,function(b){return Kb[b]})}function Ha(a){a=K(a);return{scrollLeft:a.pageXOffset,scrollTop:a.pageYOffset}}function Ia(a){return ha(U(a)).left+Ha(a).scrollLeft}function Ja(a){a=P(a);return/auto|scroll|overlay|hidden/.test(a.overflow+a.overflowY+a.overflowX)}function bb(a){return 0<=["html","body","#document"].indexOf(M(a))?a.ownerDocument.body:F(a)&&Ja(a)?a:bb(wa(a))}function sa(a,
+b){var c;void 0===b&&(b=[]);var d=bb(a);a=d===(null==(c=a.ownerDocument)?void 0:c.body);c=K(d);d=a?[c].concat(c.visualViewport||[],Ja(d)?d:[]):d;b=b.concat(d);return a?b:b.concat(sa(wa(d)))}function Ka(a){return Object.assign({},a,{left:a.x,top:a.y,right:a.x+a.width,bottom:a.y+a.height})}function cb(a,b){if("viewport"===b){b=K(a);var c=U(a);b=b.visualViewport;var d=c.clientWidth;c=c.clientHeight;var e=0,f=0;b&&(d=b.width,c=b.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(e=b.offsetLeft,
+f=b.offsetTop));a={width:d,height:c,x:e+Ia(a),y:f};a=Ka(a)}else fa(b)?(a=ha(b),a.top+=b.clientTop,a.left+=b.clientLeft,a.bottom=a.top+b.clientHeight,a.right=a.left+b.clientWidth,a.width=b.clientWidth,a.height=b.clientHeight,a.x=a.left,a.y=a.top):(f=U(a),a=U(f),d=Ha(f),b=null==(c=f.ownerDocument)?void 0:c.body,c=L(a.scrollWidth,a.clientWidth,b?b.scrollWidth:0,b?b.clientWidth:0),e=L(a.scrollHeight,a.clientHeight,b?b.scrollHeight:0,b?b.clientHeight:0),f=-d.scrollLeft+Ia(f),d=-d.scrollTop,"rtl"===P(b||
+a).direction&&(f+=L(a.clientWidth,b?b.clientWidth:0)-c),a=Ka({width:c,height:e,x:f,y:d}));return a}function Lb(a){var b=sa(wa(a)),c=0<=["absolute","fixed"].indexOf(P(a).position)&&F(a)?ra(a):a;return fa(c)?b.filter(function(d){return fa(d)&&Wa(d,c)&&"body"!==M(d)}):[]}function Mb(a,b,c){b="clippingParents"===b?Lb(a):[].concat(b);c=[].concat(b,[c]);c=c.reduce(function(d,e){e=cb(a,e);d.top=L(e.top,d.top);d.right=V(e.right,d.right);d.bottom=V(e.bottom,d.bottom);d.left=L(e.left,d.left);return d},cb(a,
+c[0]));c.width=c.right-c.left;c.height=c.bottom-c.top;c.x=c.left;c.y=c.top;return c}function db(a){var b=a.reference,c=a.element,d=(a=a.placement)?N(a):null;a=a?ja(a):null;var e=b.x+b.width/2-c.width/2,f=b.y+b.height/2-c.height/2;switch(d){case "top":e={x:e,y:b.y-c.height};break;case "bottom":e={x:e,y:b.y+b.height};break;case "right":e={x:b.x+b.width,y:f};break;case "left":e={x:b.x-c.width,y:f};break;default:e={x:b.x,y:b.y}}d=d?Ga(d):null;if(null!=d)switch(f="y"===d?"height":"width",a){case "start":e[d]-=
+b[f]/2-c[f]/2;break;case "end":e[d]+=b[f]/2-c[f]/2}return e}function ta(a,b){void 0===b&&(b={});var c=b;b=c.placement;b=void 0===b?a.placement:b;var d=c.boundary,e=void 0===d?"clippingParents":d;d=c.rootBoundary;var f=void 0===d?"viewport":d;d=c.elementContext;d=void 0===d?"popper":d;var g=c.altBoundary,l=void 0===g?!1:g;c=c.padding;c=void 0===c?0:c;c=Ya("number"!==typeof c?c:Za(c,ua));g=a.rects.popper;l=a.elements[l?"popper"===d?"reference":"popper":d];e=Mb(fa(l)?l:l.contextElement||U(a.elements.popper),
+e,f);f=ha(a.elements.reference);l=db({reference:f,element:g,strategy:"absolute",placement:b});g=Ka(Object.assign({},g,l));f="popper"===d?g:f;var m={top:e.top-f.top+c.top,bottom:f.bottom-e.bottom+c.bottom,left:e.left-f.left+c.left,right:f.right-e.right+c.right};a=a.modifiersData.offset;if("popper"===d&&a){var k=a[b];Object.keys(m).forEach(function(p){var q=0<=["right","bottom"].indexOf(p)?1:-1,n=0<=["top","bottom"].indexOf(p)?"y":"x";m[p]+=k[n]*q})}return m}function Nb(a,b){void 0===b&&(b={});var c=
+b.boundary,d=b.rootBoundary,e=b.padding,f=b.flipVariations,g=b.allowedAutoPlacements,l=void 0===g?eb:g,m=ja(b.placement);b=m?f?fb:fb.filter(function(p){return ja(p)===m}):ua;f=b.filter(function(p){return 0<=l.indexOf(p)});0===f.length&&(f=b);var k=f.reduce(function(p,q){p[q]=ta(a,{placement:q,boundary:c,rootBoundary:d,padding:e})[N(q)];return p},{});return Object.keys(k).sort(function(p,q){return k[p]-k[q]})}function Ob(a){if("auto"===N(a))return[];var b=xa(a);return[ab(a),b,ab(b)]}function gb(a,
+b,c){void 0===c&&(c={x:0,y:0});return{top:a.top-b.height-c.y,right:a.right-b.width+c.x,bottom:a.bottom-b.height+c.y,left:a.left-b.width-c.x}}function hb(a){return["top","right","bottom","left"].some(function(b){return 0<=a[b]})}function Pb(a,b,c){void 0===c&&(c=!1);var d=F(b),e;if(e=F(b)){var f=b.getBoundingClientRect();e=ia(f.width)/b.offsetWidth||1;f=ia(f.height)/b.offsetHeight||1;e=1!==e||1!==f}f=e;e=U(b);a=ha(a,f);f={scrollLeft:0,scrollTop:0};var g={x:0,y:0};if(d||!d&&!c){if("body"!==M(b)||Ja(e))f=
+b!==K(b)&&F(b)?{scrollLeft:b.scrollLeft,scrollTop:b.scrollTop}:Ha(b);F(b)?(g=ha(b,!0),g.x+=b.clientLeft,g.y+=b.clientTop):e&&(g.x=Ia(e))}return{x:a.left+f.scrollLeft-g.x,y:a.top+f.scrollTop-g.y,width:a.width,height:a.height}}function Qb(a){function b(f){d.add(f.name);[].concat(f.requires||[],f.requiresIfExists||[]).forEach(function(g){d.has(g)||(g=c.get(g))&&b(g)});e.push(f)}var c=new Map,d=new Set,e=[];a.forEach(function(f){c.set(f.name,f)});a.forEach(function(f){d.has(f.name)||b(f)});return e}function Rb(a){var b=
+Qb(a);return Sb.reduce(function(c,d){return c.concat(b.filter(function(e){return e.phase===d}))},[])}function Tb(a){var b;return function(){b||(b=new Promise(function(c){Promise.resolve().then(function(){b=void 0;c(a())})}));return b}}function Ub(a){var b=a.reduce(function(c,d){var e=c[d.name];c[d.name]=e?Object.assign({},e,d,{options:Object.assign({},e.options,d.options),data:Object.assign({},e.data,d.data)}):d;return c},{});return Object.keys(b).map(function(c){return b[c]})}function ib(){for(var a=
+arguments.length,b=Array(a),c=0;c<a;c++)b[c]=arguments[c];return!b.some(function(d){return!(d&&"function"===typeof d.getBoundingClientRect)})}function La(){La=Object.assign||function(a){for(var b=1;b<arguments.length;b++){var c=arguments[b],d;for(d in c)Object.prototype.hasOwnProperty.call(c,d)&&(a[d]=c[d])}return a};return La.apply(this,arguments)}function Vb(){return[{name:"applyStyles",fn(a){let {state:b}=a;Object.keys(b.elements).forEach(c=>{if("popper"===c){var d=b.attributes[c]||{},e=b.elements[c];
+Object.assign(e.style,{position:"fixed",left:"50%",top:"50%",transform:"translate(-50%, -50%)"});Object.keys(d).forEach(f=>{let g=d[f];!1===g?e.removeAttribute(f):e.setAttribute(f,!0===g?"":g)})}})}},{name:"computeStyles",options:{adaptive:!1}}]}function Wb(a){let b=Vb(),c={placement:"top",strategy:"fixed",modifiers:[{name:"focusAfterRender",enabled:!0,phase:"afterWrite",fn(){setTimeout(()=>{a.el&&a.el.focus()},300)}}]};return c=La({},c,{modifiers:Array.from(new Set([...c.modifiers,...b]))})}function jb(a){return qa(a)&&
+""!==a?"-"!==a.charAt(a.length-1)?`${a}-`:a:""}function Ma(a){a=a.options.attachTo||{};let b=Object.assign({},a);if(qa(a.element)){try{b.element=document.querySelector(a.element)}catch(c){}b.element||console.error(`The element for this Shepherd step was not found ${a.element}`)}return b}function Na(){let a=Date.now();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,b=>{let c=(a+16*Math.random())%16|0;a=Math.floor(a/16);return("x"==b?c:c&3|8).toString(16)})}function Xb(a,b){let c={modifiers:[{name:"preventOverflow",
+options:{altAxis:!0,tether:!1}},{name:"focusAfterRender",enabled:!0,phase:"afterWrite",fn(){setTimeout(()=>{b.el&&b.el.focus()},300)}}],strategy:"absolute"};b.isCentered()?c=Wb(b):c.placement=a.on;(a=b.tour&&b.tour.options&&b.tour.options.defaultStepOptions)&&(c=kb(a,c));return c=kb(b.options,c)}function kb(a,b){if(a.popperOptions){let c=Object.assign({},b,a.popperOptions);if(a.popperOptions.modifiers&&0<a.popperOptions.modifiers.length){let d=a.popperOptions.modifiers.map(e=>e.name);b=b.modifiers.filter(e=>
+!d.includes(e.name));c.modifiers=Array.from(new Set([...b,...a.popperOptions.modifiers]))}return c}return b}function G(){}function Yb(a,b){for(let c in b)a[c]=b[c];return a}function ka(a){return a()}function lb(a){return"function"===typeof a}function Q(a,b){return a!=a?b==b:a!==b||a&&"object"===typeof a||"function"===typeof a}function H(a){a.parentNode.removeChild(a)}function mb(a){return document.createElementNS("http://www.w3.org/2000/svg",a)}function ya(a,b,c,d){a.addEventListener(b,c,d);return()=>
+a.removeEventListener(b,c,d)}function B(a,b,c){null==c?a.removeAttribute(b):a.getAttribute(b)!==c&&a.setAttribute(b,c)}function nb(a,b){let c=Object.getOwnPropertyDescriptors(a.__proto__);for(let d in b)null==b[d]?a.removeAttribute(d):"style"===d?a.style.cssText=b[d]:"__value"===d?a.value=a[d]=b[d]:c[d]&&c[d].set?a[d]=b[d]:B(a,d,b[d])}function la(a,b,c){a.classList[c?"add":"remove"](b)}function za(){if(!R)throw Error("Function called outside component initialization");return R}function Oa(a){Aa.push(a)}
+function ob(){let a=R;do{for(;Ba<va.length;){var b=va[Ba];Ba++;R=b;b=b.$$;if(null!==b.fragment){b.update();b.before_update.forEach(ka);var c=b.dirty;b.dirty=[-1];b.fragment&&b.fragment.p(b.ctx,c);b.after_update.forEach(Oa)}}R=null;for(Ba=va.length=0;ma.length;)ma.pop()();for(b=0;b<Aa.length;b+=1)c=Aa[b],Pa.has(c)||(Pa.add(c),c());Aa.length=0}while(va.length);for(;pb.length;)pb.pop()();Qa=!1;Pa.clear();R=a}function Z(){aa={r:0,c:[],p:aa}}function ba(){aa.r||aa.c.forEach(ka);aa=aa.p}function z(a,b){a&&
+a.i&&(Ca.delete(a),a.i(b))}function C(a,b,c,d){a&&a.o&&!Ca.has(a)&&(Ca.add(a),aa.c.push(()=>{Ca.delete(a);d&&(c&&a.d(1),d())}),a.o(b))}function ca(a){a&&a.c()}function W(a,b,c,d){let {fragment:e,on_mount:f,on_destroy:g,after_update:l}=a.$$;e&&e.m(b,c);d||Oa(()=>{let m=f.map(ka).filter(lb);g?g.push(...m):m.forEach(ka);a.$$.on_mount=[]});l.forEach(Oa)}function X(a,b){a=a.$$;null!==a.fragment&&(a.on_destroy.forEach(ka),a.fragment&&a.fragment.d(b),a.on_destroy=a.fragment=null,a.ctx=[])}function S(a,b,
+c,d,e,f,g,l){void 0===l&&(l=[-1]);let m=R;R=a;let k=a.$$={fragment:null,ctx:null,props:f,update:G,not_equal:e,bound:Object.create(null),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(b.context||(m?m.$$.context:[])),callbacks:Object.create(null),dirty:l,skip_bound:!1,root:b.target||m.$$.root};g&&g(k.root);let p=!1;k.ctx=c?c(a,b.props||{},function(q,n){let r=(2>=arguments.length?0:arguments.length-2)?2>=arguments.length?void 0:arguments[2]:n;if(k.ctx&&e(k.ctx[q],
+k.ctx[q]=r)){if(!k.skip_bound&&k.bound[q])k.bound[q](r);p&&(-1===a.$$.dirty[0]&&(va.push(a),Qa||(Qa=!0,Zb.then(ob)),a.$$.dirty.fill(0)),a.$$.dirty[q/31|0]|=1<<q%31)}return n}):[];k.update();p=!0;k.before_update.forEach(ka);k.fragment=d?d(k.ctx):!1;b.target&&(b.hydrate?(c=Array.from(b.target.childNodes),k.fragment&&k.fragment.l(c),c.forEach(H)):k.fragment&&k.fragment.c(),b.intro&&z(a.$$.fragment),W(a,b.target,b.anchor,b.customElement),ob());R=m}function $b(a){let b,c,d,e,f;return{c(){b=document.createElement("button");
+B(b,"aria-label",c=a[3]?a[3]:null);B(b,"class",d=`${a[1]||""} shepherd-button ${a[4]?"shepherd-button-secondary":""}`);b.disabled=a[2];B(b,"tabindex","0")},m(g,l){g.insertBefore(b,l||null);b.innerHTML=a[5];e||(f=ya(b,"click",function(){lb(a[0])&&a[0].apply(this,arguments)}),e=!0)},p(g,l){[l]=l;a=g;l&32&&(b.innerHTML=a[5]);l&8&&c!==(c=a[3]?a[3]:null)&&B(b,"aria-label",c);l&18&&d!==(d=`${a[1]||""} shepherd-button ${a[4]?"shepherd-button-secondary":""}`)&&B(b,"class",d);l&4&&(b.disabled=a[2])},i:G,o:G,
+d(g){g&&H(b);e=!1;f()}}}function ac(a,b,c){function d(n){return ea(n)?n.call(f):n}let {config:e,step:f}=b,g,l,m,k,p,q;a.$$set=n=>{"config"in n&&c(6,e=n.config);"step"in n&&c(7,f=n.step)};a.$$.update=()=>{a.$$.dirty&192&&(c(0,g=e.action?e.action.bind(f.tour):null),c(1,l=e.classes),c(2,m=e.disabled?d(e.disabled):!1),c(3,k=e.label?d(e.label):null),c(4,p=e.secondary),c(5,q=e.text?d(e.text):null))};return[g,l,m,k,p,q,e,f]}function qb(a,b,c){a=a.slice();a[2]=b[c];return a}function rb(a){let b,c,d=a[1],
+e=[];for(let g=0;g<d.length;g+=1)e[g]=sb(qb(a,d,g));let f=g=>C(e[g],1,1,()=>{e[g]=null});return{c(){for(let g=0;g<e.length;g+=1)e[g].c();b=document.createTextNode("")},m(g,l){for(let m=0;m<e.length;m+=1)e[m].m(g,l);g.insertBefore(b,l||null);c=!0},p(g,l){if(l&3){d=g[1];let m;for(m=0;m<d.length;m+=1){let k=qb(g,d,m);e[m]?(e[m].p(k,l),z(e[m],1)):(e[m]=sb(k),e[m].c(),z(e[m],1),e[m].m(b.parentNode,b))}Z();for(m=d.length;m<e.length;m+=1)f(m);ba()}},i(g){if(!c){for(g=0;g<d.length;g+=1)z(e[g]);c=!0}},o(g){e=
+e.filter(Boolean);for(g=0;g<e.length;g+=1)C(e[g]);c=!1},d(g){var l=e;for(let m=0;m<l.length;m+=1)l[m]&&l[m].d(g);g&&H(b)}}}function sb(a){let b,c;b=new bc({props:{config:a[2],step:a[0]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&2&&(f.config=d[2]);e&1&&(f.step=d[0]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function cc(a){let b,c,d=a[1]&&rb(a);return{c(){b=document.createElement("footer");d&&d.c();B(b,"class","shepherd-footer")},
+m(e,f){e.insertBefore(b,f||null);d&&d.m(b,null);c=!0},p(e,f){[f]=f;e[1]?d?(d.p(e,f),f&2&&z(d,1)):(d=rb(e),d.c(),z(d,1),d.m(b,null)):d&&(Z(),C(d,1,1,()=>{d=null}),ba())},i(e){c||(z(d),c=!0)},o(e){C(d);c=!1},d(e){e&&H(b);d&&d.d()}}}function dc(a,b,c){let d,{step:e}=b;a.$$set=f=>{"step"in f&&c(0,e=f.step)};a.$$.update=()=>{a.$$.dirty&1&&c(1,d=e.options.buttons)};return[e,d]}function ec(a){let b,c,d,e,f;return{c(){b=document.createElement("button");c=document.createElement("span");c.textContent="\u00d7";
+B(c,"aria-hidden","true");B(b,"aria-label",d=a[0].label?a[0].label:"Close Tour");B(b,"class","shepherd-cancel-icon");B(b,"type","button")},m(g,l){g.insertBefore(b,l||null);b.appendChild(c);e||(f=ya(b,"click",a[1]),e=!0)},p(g,l){[l]=l;l&1&&d!==(d=g[0].label?g[0].label:"Close Tour")&&B(b,"aria-label",d)},i:G,o:G,d(g){g&&H(b);e=!1;f()}}}function fc(a,b,c){let {cancelIcon:d,step:e}=b;a.$$set=f=>{"cancelIcon"in f&&c(0,d=f.cancelIcon);"step"in f&&c(2,e=f.step)};return[d,f=>{f.preventDefault();e.cancel()},
+e]}function gc(a){let b;return{c(){b=document.createElement("h3");B(b,"id",a[1]);B(b,"class","shepherd-title")},m(c,d){c.insertBefore(b,d||null);a[3](b)},p(c,d){[d]=d;d&2&&B(b,"id",c[1])},i:G,o:G,d(c){c&&H(b);a[3](null)}}}function hc(a,b,c){let {labelId:d,element:e,title:f}=b;za().$$.after_update.push(()=>{ea(f)&&c(2,f=f());c(0,e.innerHTML=f,e)});a.$$set=g=>{"labelId"in g&&c(1,d=g.labelId);"element"in g&&c(0,e=g.element);"title"in g&&c(2,f=g.title)};return[e,d,f,function(g){ma[g?"unshift":"push"](()=>
+{e=g;c(0,e)})}]}function tb(a){let b,c;b=new ic({props:{labelId:a[0],title:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&1&&(f.labelId=d[0]);e&4&&(f.title=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function ub(a){let b,c;b=new jc({props:{cancelIcon:a[3],step:a[1]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&8&&(f.cancelIcon=d[3]);e&2&&(f.step=d[1]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),
+c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function kc(a){let b,c,d,e=a[2]&&tb(a),f=a[3]&&a[3].enabled&&ub(a);return{c(){b=document.createElement("header");e&&e.c();c=document.createTextNode(" ");f&&f.c();B(b,"class","shepherd-header")},m(g,l){g.insertBefore(b,l||null);e&&e.m(b,null);b.appendChild(c);f&&f.m(b,null);d=!0},p(g,l){[l]=l;g[2]?e?(e.p(g,l),l&4&&z(e,1)):(e=tb(g),e.c(),z(e,1),e.m(b,c)):e&&(Z(),C(e,1,1,()=>{e=null}),ba());g[3]&&g[3].enabled?f?(f.p(g,l),l&8&&z(f,1)):(f=ub(g),f.c(),
+z(f,1),f.m(b,null)):f&&(Z(),C(f,1,1,()=>{f=null}),ba())},i(g){d||(z(e),z(f),d=!0)},o(g){C(e);C(f);d=!1},d(g){g&&H(b);e&&e.d();f&&f.d()}}}function lc(a,b,c){let {labelId:d,step:e}=b,f,g;a.$$set=l=>{"labelId"in l&&c(0,d=l.labelId);"step"in l&&c(1,e=l.step)};a.$$.update=()=>{a.$$.dirty&2&&(c(2,f=e.options.title),c(3,g=e.options.cancelIcon))};return[d,e,f,g]}function mc(a){let b;return{c(){b=document.createElement("div");B(b,"class","shepherd-text");B(b,"id",a[1])},m(c,d){c.insertBefore(b,d||null);a[3](b)},
+p(c,d){[d]=d;d&2&&B(b,"id",c[1])},i:G,o:G,d(c){c&&H(b);a[3](null)}}}function nc(a,b,c){let {descriptionId:d,element:e,step:f}=b;za().$$.after_update.push(()=>{let {text:g}=f.options;ea(g)&&(g=g.call(f));g instanceof HTMLElement?e.appendChild(g):c(0,e.innerHTML=g,e)});a.$$set=g=>{"descriptionId"in g&&c(1,d=g.descriptionId);"element"in g&&c(0,e=g.element);"step"in g&&c(2,f=g.step)};return[e,d,f,function(g){ma[g?"unshift":"push"](()=>{e=g;c(0,e)})}]}function vb(a){let b,c;b=new oc({props:{labelId:a[1],
+step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&2&&(f.labelId=d[1]);e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function wb(a){let b,c;b=new pc({props:{descriptionId:a[0],step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&1&&(f.descriptionId=d[0]);e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function xb(a){let b,
+c;b=new qc({props:{step:a[2]}});return{c(){ca(b.$$.fragment)},m(d,e){W(b,d,e);c=!0},p(d,e){let f={};e&4&&(f.step=d[2]);b.$set(f)},i(d){c||(z(b.$$.fragment,d),c=!0)},o(d){C(b.$$.fragment,d);c=!1},d(d){X(b,d)}}}function rc(a){let b,c=void 0!==a[2].options.title||a[2].options.cancelIcon&&a[2].options.cancelIcon.enabled,d,e=void 0!==a[2].options.text,f,g=Array.isArray(a[2].options.buttons)&&a[2].options.buttons.length,l,m=c&&vb(a),k=e&&wb(a),p=g&&xb(a);return{c(){b=document.createElement("div");m&&m.c();
+d=document.createTextNode(" ");k&&k.c();f=document.createTextNode(" ");p&&p.c();B(b,"class","shepherd-content")},m(q,n){q.insertBefore(b,n||null);m&&m.m(b,null);b.appendChild(d);k&&k.m(b,null);b.appendChild(f);p&&p.m(b,null);l=!0},p(q,n){[n]=n;n&4&&(c=void 0!==q[2].options.title||q[2].options.cancelIcon&&q[2].options.cancelIcon.enabled);c?m?(m.p(q,n),n&4&&z(m,1)):(m=vb(q),m.c(),z(m,1),m.m(b,d)):m&&(Z(),C(m,1,1,()=>{m=null}),ba());n&4&&(e=void 0!==q[2].options.text);e?k?(k.p(q,n),n&4&&z(k,1)):(k=wb(q),
+k.c(),z(k,1),k.m(b,f)):k&&(Z(),C(k,1,1,()=>{k=null}),ba());n&4&&(g=Array.isArray(q[2].options.buttons)&&q[2].options.buttons.length);g?p?(p.p(q,n),n&4&&z(p,1)):(p=xb(q),p.c(),z(p,1),p.m(b,null)):p&&(Z(),C(p,1,1,()=>{p=null}),ba())},i(q){l||(z(m),z(k),z(p),l=!0)},o(q){C(m);C(k);C(p);l=!1},d(q){q&&H(b);m&&m.d();k&&k.d();p&&p.d()}}}function sc(a,b,c){let {descriptionId:d,labelId:e,step:f}=b;a.$$set=g=>{"descriptionId"in g&&c(0,d=g.descriptionId);"labelId"in g&&c(1,e=g.labelId);"step"in g&&c(2,f=g.step)};
+return[d,e,f]}function yb(a){let b;return{c(){b=document.createElement("div");B(b,"class","shepherd-arrow");B(b,"data-popper-arrow","")},m(c,d){c.insertBefore(b,d||null)},d(c){c&&H(b)}}}function tc(a){let b,c,d,e,f,g,l,m,k=a[4].options.arrow&&a[4].options.attachTo&&a[4].options.attachTo.element&&a[4].options.attachTo.on&&yb();d=new uc({props:{descriptionId:a[2],labelId:a[3],step:a[4]}});let p=[{"aria-describedby":e=void 0!==a[4].options.text?a[2]:null},{"aria-labelledby":f=a[4].options.title?a[3]:
+null},a[1],{role:"dialog"},{tabindex:"0"}],q={};for(let n=0;n<p.length;n+=1)q=Yb(q,p[n]);return{c(){b=document.createElement("div");k&&k.c();c=document.createTextNode(" ");ca(d.$$.fragment);nb(b,q);la(b,"shepherd-has-cancel-icon",a[5]);la(b,"shepherd-has-title",a[6]);la(b,"shepherd-element",!0)},m(n,r){n.insertBefore(b,r||null);k&&k.m(b,null);b.appendChild(c);W(d,b,null);a[13](b);g=!0;l||(m=ya(b,"keydown",a[7]),l=!0)},p(n,r){var [x]=r;n[4].options.arrow&&n[4].options.attachTo&&n[4].options.attachTo.element&&
+n[4].options.attachTo.on?k||(k=yb(),k.c(),k.m(b,c)):k&&(k.d(1),k=null);r={};x&4&&(r.descriptionId=n[2]);x&8&&(r.labelId=n[3]);x&16&&(r.step=n[4]);d.$set(r);r=b;x=[(!g||x&20&&e!==(e=void 0!==n[4].options.text?n[2]:null))&&{"aria-describedby":e},(!g||x&24&&f!==(f=n[4].options.title?n[3]:null))&&{"aria-labelledby":f},x&2&&n[1],{role:"dialog"},{tabindex:"0"}];let h={},t={},v={$$scope:1},A=p.length;for(;A--;){let u=p[A],w=x[A];if(w){for(let y in u)y in w||(t[y]=1);for(let y in w)v[y]||(h[y]=w[y],v[y]=
+1);p[A]=w}else for(let y in u)v[y]=1}for(let u in t)u in h||(h[u]=void 0);nb(r,q=h);la(b,"shepherd-has-cancel-icon",n[5]);la(b,"shepherd-has-title",n[6]);la(b,"shepherd-element",!0)},i(n){g||(z(d.$$.fragment,n),g=!0)},o(n){C(d.$$.fragment,n);g=!1},d(n){n&&H(b);k&&k.d();X(d);a[13](null);l=!1;m()}}}function zb(a){return a.split(" ").filter(b=>!!b.length)}function vc(a,b,c){let {classPrefix:d,element:e,descriptionId:f,firstFocusableElement:g,focusableElements:l,labelId:m,lastFocusableElement:k,step:p,
+dataStepId:q}=b,n,r,x;za().$$.on_mount.push(()=>{c(1,q={[`data-${d}shepherd-step-id`]:p.id});c(9,l=e.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'));c(8,g=l[0]);c(10,k=l[l.length-1])});za().$$.after_update.push(()=>{if(x!==p.options.classes){var h=x;qa(h)&&(h=zb(h),h.length&&e.classList.remove(...h));h=x=p.options.classes;qa(h)&&(h=zb(h),h.length&&e.classList.add(...h))}});a.$$set=h=>{"classPrefix"in
+h&&c(11,d=h.classPrefix);"element"in h&&c(0,e=h.element);"descriptionId"in h&&c(2,f=h.descriptionId);"firstFocusableElement"in h&&c(8,g=h.firstFocusableElement);"focusableElements"in h&&c(9,l=h.focusableElements);"labelId"in h&&c(3,m=h.labelId);"lastFocusableElement"in h&&c(10,k=h.lastFocusableElement);"step"in h&&c(4,p=h.step);"dataStepId"in h&&c(1,q=h.dataStepId)};a.$$.update=()=>{a.$$.dirty&16&&(c(5,n=p.options&&p.options.cancelIcon&&p.options.cancelIcon.enabled),c(6,r=p.options&&p.options.title))};
+return[e,q,f,m,p,n,r,h=>{const {tour:t}=p;switch(h.keyCode){case 9:if(0===l.length){h.preventDefault();break}if(h.shiftKey){if(document.activeElement===g||document.activeElement.classList.contains("shepherd-element"))h.preventDefault(),k.focus()}else document.activeElement===k&&(h.preventDefault(),g.focus());break;case 27:t.options.exitOnEsc&&p.cancel();break;case 37:t.options.keyboardNavigation&&t.back();break;case 39:t.options.keyboardNavigation&&t.next()}},g,l,k,d,()=>e,function(h){ma[h?"unshift":
+"push"](()=>{e=h;c(0,e)})}]}function wc(a){a&&({steps:a}=a,a.forEach(b=>{b.options&&!1===b.options.canClickTarget&&b.options.attachTo&&b.target instanceof HTMLElement&&b.target.classList.remove("shepherd-target-click-disabled")}))}function xc(a){let b,c,d,e,f;return{c(){b=mb("svg");c=mb("path");B(c,"d",a[2]);B(b,"class",d=`${a[1]?"shepherd-modal-is-visible":""} shepherd-modal-overlay-container`)},m(g,l){g.insertBefore(b,l||null);b.appendChild(c);a[11](b);e||(f=ya(b,"touchmove",a[3]),e=!0)},p(g,l){[l]=
+l;l&4&&B(c,"d",g[2]);l&2&&d!==(d=`${g[1]?"shepherd-modal-is-visible":""} shepherd-modal-overlay-container`)&&B(b,"class",d)},i:G,o:G,d(g){g&&H(b);a[11](null);e=!1;f()}}}function Ab(a){if(!a)return null;let b=a instanceof HTMLElement&&window.getComputedStyle(a).overflowY;return"hidden"!==b&&"visible"!==b&&a.scrollHeight>=a.clientHeight?a:Ab(a.parentElement)}function yc(a,b,c){function d(){c(4,p={width:0,height:0,x:0,y:0,r:0})}function e(){c(1,q=!1);l()}function f(h,t,v,A){void 0===h&&(h=0);void 0===
+t&&(t=0);if(A){var u=A.getBoundingClientRect();let y=u.y||u.top;u=u.bottom||y+u.height;if(v){var w=v.getBoundingClientRect();v=w.y||w.top;w=w.bottom||v+w.height;y=Math.max(y,v);u=Math.min(u,w)}let {y:Y,height:E}={y,height:Math.max(u-y,0)},{x:I,width:D,left:na}=A.getBoundingClientRect();c(4,p={width:D+2*h,height:E+2*h,x:(I||na)-h,y:Y-h,r:t})}else d()}function g(){c(1,q=!0)}function l(){n&&(cancelAnimationFrame(n),n=void 0);window.removeEventListener("touchmove",x,{passive:!1})}function m(h){let {modalOverlayOpeningPadding:t,
+modalOverlayOpeningRadius:v}=h.options,A=Ab(h.target),u=()=>{n=void 0;f(t,v,A,h.target);n=requestAnimationFrame(u)};u();window.addEventListener("touchmove",x,{passive:!1})}let {element:k,openingProperties:p}=b;Na();let q=!1,n=void 0,r;d();let x=h=>{h.preventDefault()};a.$$set=h=>{"element"in h&&c(0,k=h.element);"openingProperties"in h&&c(4,p=h.openingProperties)};a.$$.update=()=>{if(a.$$.dirty&16){let {width:h,height:t,x:v=0,y:A=0,r:u=0}=p,{innerWidth:w,innerHeight:y}=window;c(2,r=`M${w},${y}\
+H0\
+V0\
+H${w}\
+V${y}\
+Z\
+M${v+u},${A}\
+a${u},${u},0,0,0-${u},${u}\
+V${t+A-u}\
+a${u},${u},0,0,0,${u},${u}\
+H${h+v-u}\
+a${u},${u},0,0,0,${u}-${u}\
+V${A+u}\
+a${u},${u},0,0,0-${u}-${u}\
+Z`)}};return[k,q,r,h=>{h.stopPropagation()},p,()=>k,d,e,f,function(h){l();h.tour.options.useModalOverlay?(m(h),g()):e()},g,function(h){ma[h?"unshift":"push"](()=>{k=h;c(0,k)})}]}var Fb=function(a){var b;if(b=!!a&&"object"===typeof a)b=Object.prototype.toString.call(a),b=!("[object RegExp]"===b||"[object Date]"===b||a.$$typeof===zc);return b},zc="function"===typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;da.all=function(a,b){if(!Array.isArray(a))throw Error("first argument should be an array");
+return a.reduce(function(c,d){return da(c,d,b)},{})};var Ac=da;class Ra{on(a,b,c,d){void 0===d&&(d=!1);void 0===this.bindings&&(this.bindings={});void 0===this.bindings[a]&&(this.bindings[a]=[]);this.bindings[a].push({handler:b,ctx:c,once:d});return this}once(a,b,c){return this.on(a,b,c,!0)}off(a,b){if(void 0===this.bindings||void 0===this.bindings[a])return this;void 0===b?delete this.bindings[a]:this.bindings[a].forEach((c,d)=>{c.handler===b&&this.bindings[a].splice(d,1)});return this}trigger(a){for(var b=
+arguments.length,c=Array(1<b?b-1:0),d=1;d<b;d++)c[d-1]=arguments[d];void 0!==this.bindings&&this.bindings[a]&&this.bindings[a].forEach((e,f)=>{let {ctx:g,handler:l,once:m}=e;l.apply(g||this,c);m&&this.bindings[a].splice(f,1)});return this}}var ua=["top","bottom","right","left"],fb=ua.reduce(function(a,b){return a.concat([b+"-start",b+"-end"])},[]),eb=[].concat(ua,["auto"]).reduce(function(a,b){return a.concat([b,b+"-start",b+"-end"])},[]),Sb="beforeRead read afterRead beforeMain main afterMain beforeWrite write afterWrite".split(" "),
+L=Math.max,V=Math.min,ia=Math.round,Ib={top:"auto",right:"auto",bottom:"auto",left:"auto"},Da={passive:!0},Jb={left:"right",right:"left",bottom:"top",top:"bottom"},Kb={start:"end",end:"start"},Bb={placement:"bottom",modifiers:[],strategy:"absolute"},Bc=function(a){void 0===a&&(a={});var b=a.defaultModifiers,c=void 0===b?[]:b;a=a.defaultOptions;var d=void 0===a?Bb:a;return function(e,f,g){function l(){k.orderedModifiers.forEach(function(r){var x=r.name,h=r.options;h=void 0===h?{}:h;r=r.effect;"function"===
+typeof r&&(x=r({state:k,name:x,instance:n,options:h}),p.push(x||function(){}))})}function m(){p.forEach(function(r){return r()});p=[]}void 0===g&&(g=d);var k={placement:"bottom",orderedModifiers:[],options:Object.assign({},Bb,d),modifiersData:{},elements:{reference:e,popper:f},attributes:{},styles:{}},p=[],q=!1,n={state:k,setOptions:function(r){r="function"===typeof r?r(k.options):r;m();k.options=Object.assign({},d,k.options,r);k.scrollParents={reference:fa(e)?sa(e):e.contextElement?sa(e.contextElement):
+[],popper:sa(f)};r=Rb(Ub([].concat(c,k.options.modifiers)));k.orderedModifiers=r.filter(function(x){return x.enabled});l();return n.update()},forceUpdate:function(){if(!q){var r=k.elements,x=r.reference;r=r.popper;if(ib(x,r))for(k.rects={reference:Pb(x,ra(r),"fixed"===k.options.strategy),popper:Fa(r)},k.reset=!1,k.placement=k.options.placement,k.orderedModifiers.forEach(function(v){return k.modifiersData[v.name]=Object.assign({},v.data)}),x=0;x<k.orderedModifiers.length;x++)if(!0===k.reset)k.reset=
+!1,x=-1;else{var h=k.orderedModifiers[x];r=h.fn;var t=h.options;t=void 0===t?{}:t;h=h.name;"function"===typeof r&&(k=r({state:k,options:t,name:h,instance:n})||k)}}},update:Tb(function(){return new Promise(function(r){n.forceUpdate();r(k)})}),destroy:function(){m();q=!0}};if(!ib(e,f))return n;n.setOptions(g).then(function(r){if(!q&&g.onFirstUpdate)g.onFirstUpdate(r)});return n}}({defaultModifiers:[{name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(a){var b=a.state,c=a.instance;
+a=a.options;var d=a.scroll,e=void 0===d?!0:d;a=a.resize;var f=void 0===a?!0:a,g=K(b.elements.popper),l=[].concat(b.scrollParents.reference,b.scrollParents.popper);e&&l.forEach(function(m){m.addEventListener("scroll",c.update,Da)});f&&g.addEventListener("resize",c.update,Da);return function(){e&&l.forEach(function(m){m.removeEventListener("scroll",c.update,Da)});f&&g.removeEventListener("resize",c.update,Da)}},data:{}},{name:"popperOffsets",enabled:!0,phase:"read",fn:function(a){var b=a.state;b.modifiersData[a.name]=
+db({reference:b.rects.reference,element:b.rects.popper,strategy:"absolute",placement:b.placement})},data:{}},{name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(a){var b=a.state,c=a.options;a=c.gpuAcceleration;a=void 0===a?!0:a;var d=c.adaptive;d=void 0===d?!0:d;c=c.roundOffsets;c=void 0===c?!0:c;a={placement:N(b.placement),variation:ja(b.placement),popper:b.elements.popper,popperRect:b.rects.popper,gpuAcceleration:a,isFixed:"fixed"===b.options.strategy};null!=b.modifiersData.popperOffsets&&
+(b.styles.popper=Object.assign({},b.styles.popper,$a(Object.assign({},a,{offsets:b.modifiersData.popperOffsets,position:b.options.strategy,adaptive:d,roundOffsets:c}))));null!=b.modifiersData.arrow&&(b.styles.arrow=Object.assign({},b.styles.arrow,$a(Object.assign({},a,{offsets:b.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c}))));b.attributes.popper=Object.assign({},b.attributes.popper,{"data-popper-placement":b.placement})},data:{}},{name:"applyStyles",enabled:!0,phase:"write",
+fn:function(a){var b=a.state;Object.keys(b.elements).forEach(function(c){var d=b.styles[c]||{},e=b.attributes[c]||{},f=b.elements[c];F(f)&&M(f)&&(Object.assign(f.style,d),Object.keys(e).forEach(function(g){var l=e[g];!1===l?f.removeAttribute(g):f.setAttribute(g,!0===l?"":l)}))})},effect:function(a){var b=a.state,c={popper:{position:b.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(b.elements.popper.style,c.popper);b.styles=c;b.elements.arrow&&
+Object.assign(b.elements.arrow.style,c.arrow);return function(){Object.keys(b.elements).forEach(function(d){var e=b.elements[d],f=b.attributes[d]||{};d=Object.keys(b.styles.hasOwnProperty(d)?b.styles[d]:c[d]).reduce(function(g,l){g[l]="";return g},{});F(e)&&M(e)&&(Object.assign(e.style,d),Object.keys(f).forEach(function(g){e.removeAttribute(g)}))})}},requires:["computeStyles"]},{name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(a){var b=a.state,c=a.name;a=a.options.offset;
+var d=void 0===a?[0,0]:a;a=eb.reduce(function(g,l){var m=b.rects;var k=N(l);var p=0<=["left","top"].indexOf(k)?-1:1,q="function"===typeof d?d(Object.assign({},m,{placement:l})):d;m=q[0];q=q[1];m=m||0;q=(q||0)*p;k=0<=["left","right"].indexOf(k)?{x:q,y:m}:{x:m,y:q};g[l]=k;return g},{});var e=a[b.placement],f=e.x;e=e.y;null!=b.modifiersData.popperOffsets&&(b.modifiersData.popperOffsets.x+=f,b.modifiersData.popperOffsets.y+=e);b.modifiersData[c]=a}},{name:"flip",enabled:!0,phase:"main",fn:function(a){var b=
+a.state,c=a.options;a=a.name;if(!b.modifiersData[a]._skip){var d=c.mainAxis;d=void 0===d?!0:d;var e=c.altAxis;e=void 0===e?!0:e;var f=c.fallbackPlacements,g=c.padding,l=c.boundary,m=c.rootBoundary,k=c.altBoundary,p=c.flipVariations,q=void 0===p?!0:p,n=c.allowedAutoPlacements;c=b.options.placement;p=N(c);f=f||(p!==c&&q?Ob(c):[xa(c)]);var r=[c].concat(f).reduce(function(E,I){return E.concat("auto"===N(I)?Nb(b,{placement:I,boundary:l,rootBoundary:m,padding:g,flipVariations:q,allowedAutoPlacements:n}):
+I)},[]);c=b.rects.reference;f=b.rects.popper;var x=new Map;p=!0;for(var h=r[0],t=0;t<r.length;t++){var v=r[t],A=N(v),u="start"===ja(v),w=0<=["top","bottom"].indexOf(A),y=w?"width":"height",Y=ta(b,{placement:v,boundary:l,rootBoundary:m,altBoundary:k,padding:g});u=w?u?"right":"left":u?"bottom":"top";c[y]>f[y]&&(u=xa(u));y=xa(u);w=[];d&&w.push(0>=Y[A]);e&&w.push(0>=Y[u],0>=Y[y]);if(w.every(function(E){return E})){h=v;p=!1;break}x.set(v,w)}if(p)for(d=function(E){var I=r.find(function(D){if(D=x.get(D))return D.slice(0,
+E).every(function(na){return na})});if(I)return h=I,"break"},e=q?3:1;0<e&&"break"!==d(e);e--);b.placement!==h&&(b.modifiersData[a]._skip=!0,b.placement=h,b.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}},{name:"preventOverflow",enabled:!0,phase:"main",fn:function(a){var b=a.state,c=a.options;a=a.name;var d=c.mainAxis,e=void 0===d?!0:d;d=c.altAxis;var f=void 0===d?!1:d;d=c.tether;var g=void 0===d?!0:d;d=c.tetherOffset;var l=void 0===d?0:d,m=ta(b,{boundary:c.boundary,rootBoundary:c.rootBoundary,
+padding:c.padding,altBoundary:c.altBoundary}),k=N(b.placement),p=ja(b.placement),q=!p,n=Ga(k);c="x"===n?"y":"x";d=b.modifiersData.popperOffsets;var r=b.rects.reference,x=b.rects.popper;l="function"===typeof l?l(Object.assign({},b.rects,{placement:b.placement})):l;var h="number"===typeof l?{mainAxis:l,altAxis:l}:Object.assign({mainAxis:0,altAxis:0},l),t=b.modifiersData.offset?b.modifiersData.offset[b.placement]:null;l={x:0,y:0};if(d){if(e){var v,A="y"===n?"top":"left",u="y"===n?"bottom":"right",w=
+"y"===n?"height":"width";e=d[n];var y=e+m[A],Y=e-m[u],E=g?-x[w]/2:0,I="start"===p?r[w]:x[w];p="start"===p?-x[w]:-r[w];var D=b.elements.arrow;D=g&&D?Fa(D):{width:0,height:0};var na=b.modifiersData["arrow#persistent"]?b.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0};A=na[A];u=na[u];D=L(0,V(r[w],D[w]));I=q?r[w]/2-E-D-A-h.mainAxis:I-D-A-h.mainAxis;q=q?-r[w]/2+E+D+u+h.mainAxis:p+D+u+h.mainAxis;w=(w=b.elements.arrow&&ra(b.elements.arrow))?"y"===n?w.clientTop||0:w.clientLeft||
+0:0;E=null!=(v=null==t?void 0:t[n])?v:0;v=e+q-E;y=g?V(y,e+I-E-w):y;v=g?L(Y,v):Y;v=L(y,V(e,v));d[n]=v;l[n]=v-e}if(f){var J;f=d[c];e="y"===c?"height":"width";v=f+m["x"===n?"top":"left"];m=f-m["x"===n?"bottom":"right"];k=-1!==["top","left"].indexOf(k);n=null!=(J=null==t?void 0:t[c])?J:0;J=k?v:f-r[e]-x[e]-n+h.altAxis;r=k?f+r[e]+x[e]-n-h.altAxis:m;g&&k?(J=L(J,V(f,r)),J=J>r?r:J):J=L(g?J:v,V(f,g?r:m));d[c]=J;l[c]=J-f}b.modifiersData[a]=l}},requiresIfExists:["offset"]},{name:"arrow",enabled:!0,phase:"main",
+fn:function(a){var b,c=a.state,d=a.name,e=a.options,f=c.elements.arrow,g=c.modifiersData.popperOffsets,l=N(c.placement);a=Ga(l);l=0<=["left","right"].indexOf(l)?"height":"width";if(f&&g){e=e.padding;e="function"===typeof e?e(Object.assign({},c.rects,{placement:c.placement})):e;e=Ya("number"!==typeof e?e:Za(e,ua));var m=Fa(f),k="y"===a?"top":"left",p="y"===a?"bottom":"right",q=c.rects.reference[l]+c.rects.reference[a]-g[a]-c.rects.popper[l];g=g[a]-c.rects.reference[a];f=(f=ra(f))?"y"===a?f.clientHeight||
+0:f.clientWidth||0:0;g=f/2-m[l]/2+(q/2-g/2);l=L(e[k],V(g,f-m[l]-e[p]));c.modifiersData[d]=(b={},b[a]=l,b.centerOffset=l-g,b)}},effect:function(a){var b=a.state;a=a.options.element;a=void 0===a?"[data-popper-arrow]":a;if(null!=a){if("string"===typeof a&&(a=b.elements.popper.querySelector(a),!a))return;Wa(b.elements.popper,a)&&(b.elements.arrow=a)}},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},{name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(a){var b=
+a.state;a=a.name;var c=b.rects.reference,d=b.rects.popper,e=b.modifiersData.preventOverflow,f=ta(b,{elementContext:"reference"}),g=ta(b,{altBoundary:!0});c=gb(f,c);d=gb(g,d,e);e=hb(c);g=hb(d);b.modifiersData[a]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:e,hasPopperEscaped:g};b.attributes.popper=Object.assign({},b.attributes.popper,{"data-popper-reference-hidden":e,"data-popper-escaped":g})}}]});let R,va=[],ma=[],Aa=[],pb=[],Zb=Promise.resolve(),Qa=!1,Pa=new Set,Ba=0,Ca=new Set,
+aa;class T{$destroy(){X(this,1);this.$destroy=G}$on(a,b){let c=this.$$.callbacks[a]||(this.$$.callbacks[a]=[]);c.push(b);return()=>{let d=c.indexOf(b);-1!==d&&c.splice(d,1)}}$set(a){this.$$set&&0!==Object.keys(a).length&&(this.$$.skip_bound=!0,this.$$set(a),this.$$.skip_bound=!1)}}class bc extends T{constructor(a){super();S(this,a,ac,$b,Q,{config:6,step:7})}}class qc extends T{constructor(a){super();S(this,a,dc,cc,Q,{step:0})}}class jc extends T{constructor(a){super();S(this,a,fc,ec,Q,{cancelIcon:0,
+step:2})}}class ic extends T{constructor(a){super();S(this,a,hc,gc,Q,{labelId:1,element:0,title:2})}}class oc extends T{constructor(a){super();S(this,a,lc,kc,Q,{labelId:0,step:1})}}class pc extends T{constructor(a){super();S(this,a,nc,mc,Q,{descriptionId:1,element:0,step:2})}}class uc extends T{constructor(a){super();S(this,a,sc,rc,Q,{descriptionId:0,labelId:1,step:2})}}class Cc extends T{constructor(a){super();S(this,a,vc,tc,Q,{classPrefix:11,element:0,descriptionId:2,firstFocusableElement:8,focusableElements:9,
+labelId:3,lastFocusableElement:10,step:4,dataStepId:1,getElement:12})}get getElement(){return this.$$.ctx[12]}}var Cb=function(a,b){return b={exports:{}},a(b,b.exports),b.exports}(function(a,b){(function(){a.exports={polyfill:function(){function c(h,t){this.scrollLeft=h;this.scrollTop=t}function d(h){if(null===h||"object"!==typeof h||void 0===h.behavior||"auto"===h.behavior||"instant"===h.behavior)return!0;if("object"===typeof h&&"smooth"===h.behavior)return!1;throw new TypeError("behavior member of ScrollOptions "+
+h.behavior+" is not a valid value for enumeration ScrollBehavior.");}function e(h,t){if("Y"===t)return h.clientHeight+x<h.scrollHeight;if("X"===t)return h.clientWidth+x<h.scrollWidth}function f(h,t){h=k.getComputedStyle(h,null)["overflow"+t];return"auto"===h||"scroll"===h}function g(h){var t=e(h,"Y")&&f(h,"Y");h=e(h,"X")&&f(h,"X");return t||h}function l(h){var t=(r()-h.startTime)/468;var v=.5*(1-Math.cos(Math.PI*(1<t?1:t)));t=h.startX+(h.x-h.startX)*v;v=h.startY+(h.y-h.startY)*v;h.method.call(h.scrollable,
+t,v);t===h.x&&v===h.y||k.requestAnimationFrame(l.bind(k,h))}function m(h,t,v){var A=r();if(h===p.body){var u=k;var w=k.scrollX||k.pageXOffset;h=k.scrollY||k.pageYOffset;var y=n.scroll}else u=h,w=h.scrollLeft,h=h.scrollTop,y=c;l({scrollable:u,method:y,startTime:A,startX:w,startY:h,x:t,y:v})}var k=window,p=document;if(!("scrollBehavior"in p.documentElement.style&&!0!==k.__forceSmoothScrollPolyfill__)){var q=k.HTMLElement||k.Element,n={scroll:k.scroll||k.scrollTo,scrollBy:k.scrollBy,elementScroll:q.prototype.scroll||
+c,scrollIntoView:q.prototype.scrollIntoView},r=k.performance&&k.performance.now?k.performance.now.bind(k.performance):Date.now,x=/MSIE |Trident\/|Edge\//.test(k.navigator.userAgent)?1:0;k.scroll=k.scrollTo=function(h,t){void 0!==h&&(!0===d(h)?n.scroll.call(k,void 0!==h.left?h.left:"object"!==typeof h?h:k.scrollX||k.pageXOffset,void 0!==h.top?h.top:void 0!==t?t:k.scrollY||k.pageYOffset):m.call(k,p.body,void 0!==h.left?~~h.left:k.scrollX||k.pageXOffset,void 0!==h.top?~~h.top:k.scrollY||k.pageYOffset))};
+k.scrollBy=function(h,t){void 0!==h&&(d(h)?n.scrollBy.call(k,void 0!==h.left?h.left:"object"!==typeof h?h:0,void 0!==h.top?h.top:void 0!==t?t:0):m.call(k,p.body,~~h.left+(k.scrollX||k.pageXOffset),~~h.top+(k.scrollY||k.pageYOffset)))};q.prototype.scroll=q.prototype.scrollTo=function(h,t){if(void 0!==h)if(!0===d(h)){if("number"===typeof h&&void 0===t)throw new SyntaxError("Value could not be converted");n.elementScroll.call(this,void 0!==h.left?~~h.left:"object"!==typeof h?~~h:this.scrollLeft,void 0!==
+h.top?~~h.top:void 0!==t?~~t:this.scrollTop)}else t=h.left,h=h.top,m.call(this,this,"undefined"===typeof t?this.scrollLeft:~~t,"undefined"===typeof h?this.scrollTop:~~h)};q.prototype.scrollBy=function(h,t){void 0!==h&&(!0===d(h)?n.elementScroll.call(this,void 0!==h.left?~~h.left+this.scrollLeft:~~h+this.scrollLeft,void 0!==h.top?~~h.top+this.scrollTop:~~t+this.scrollTop):this.scroll({left:~~h.left+this.scrollLeft,top:~~h.top+this.scrollTop,behavior:h.behavior}))};q.prototype.scrollIntoView=function(h){if(!0===
+d(h))n.scrollIntoView.call(this,void 0===h?!0:h);else{for(h=this;h!==p.body&&!1===g(h);)h=h.parentNode||h.host;var t=h.getBoundingClientRect(),v=this.getBoundingClientRect();h!==p.body?(m.call(this,h,h.scrollLeft+v.left-t.left,h.scrollTop+v.top-t.top),"fixed"!==k.getComputedStyle(h).position&&k.scrollBy({left:t.left,top:t.top,behavior:"smooth"})):k.scrollBy({left:v.left,top:v.top,behavior:"smooth"})}}}}}})()});Cb.polyfill;Cb.polyfill();class Sa extends Ra{constructor(a,b){void 0===b&&(b={});super(a,
+b);this.tour=a;this.classPrefix=this.tour.options?jb(this.tour.options.classPrefix):"";this.styles=a.styles;Va(this);this._setOptions(b);return this}cancel(){this.tour.cancel();this.trigger("cancel")}complete(){this.tour.complete();this.trigger("complete")}destroy(){this.tooltip&&(this.tooltip.destroy(),this.tooltip=null);this.el instanceof HTMLElement&&this.el.parentNode&&(this.el.parentNode.removeChild(this.el),this.el=null);this._updateStepTargetOnHide();this.trigger("destroy")}getTour(){return this.tour}hide(){this.tour.modal.hide();
+this.trigger("before-hide");this.el&&(this.el.hidden=!0);this._updateStepTargetOnHide();this.trigger("hide")}isCentered(){let a=Ma(this);return!a.element||!a.on}isOpen(){return!(!this.el||this.el.hidden)}show(){if(ea(this.options.beforeShowPromise)){let a=this.options.beforeShowPromise();if(void 0!==a)return a.then(()=>this._show())}this._show()}updateStepOptions(a){Object.assign(this.options,a);this.shepherdElementComponent&&this.shepherdElementComponent.$set({step:this})}getElement(){return this.el}getTarget(){return this.target}_createTooltipContent(){this.shepherdElementComponent=
+new Cc({target:this.tour.options.stepsContainer||document.body,props:{classPrefix:this.classPrefix,descriptionId:`${this.id}-description`,labelId:`${this.id}-label`,step:this,styles:this.styles}});return this.shepherdElementComponent.getElement()}_scrollTo(a){let {element:b}=Ma(this);ea(this.options.scrollToHandler)?this.options.scrollToHandler(b):b instanceof Element&&"function"===typeof b.scrollIntoView&&b.scrollIntoView(a)}_getClassOptions(a){var b=this.tour&&this.tour.options&&this.tour.options.defaultStepOptions;
+b=b&&b.classes?b.classes:"";a=[...(a.classes?a.classes:"").split(" "),...b.split(" ")];a=new Set(a);return Array.from(a).join(" ").trim()}_setOptions(a){void 0===a&&(a={});let b=this.tour&&this.tour.options&&this.tour.options.defaultStepOptions;b=Ac({},b||{});this.options=Object.assign({arrow:!0},b,a);let {when:c}=this.options;this.options.classes=this._getClassOptions(a);this.destroy();this.id=this.options.id||`step-${Na()}`;c&&Object.keys(c).forEach(d=>{this.on(d,c[d],this)})}_setupElements(){void 0!==
+this.el&&this.destroy();this.el=this._createTooltipContent();this.options.advanceOn&&Hb(this);this.tooltip&&this.tooltip.destroy();let a=Ma(this),b=a.element,c=Xb(a,this);this.isCentered()&&(b=document.body,this.shepherdElementComponent.getElement().classList.add("shepherd-centered"));this.tooltip=Bc(b,this.el,c);this.target=a.element}_show(){this.trigger("before-show");this._setupElements();this.tour.modal||this.tour._setupModal();this.tour.modal.setupForStep(this);this._styleTargetElementForStep(this);
+this.el.hidden=!1;this.options.scrollTo&&setTimeout(()=>{this._scrollTo(this.options.scrollTo)});this.el.hidden=!1;let a=this.shepherdElementComponent.getElement(),b=this.target||document.body;b.classList.add(`${this.classPrefix}shepherd-enabled`);b.classList.add(`${this.classPrefix}shepherd-target`);a.classList.add("shepherd-enabled");this.trigger("show")}_styleTargetElementForStep(a){let b=a.target;b&&(a.options.highlightClass&&b.classList.add(a.options.highlightClass),b.classList.remove("shepherd-target-click-disabled"),
+!1===a.options.canClickTarget&&b.classList.add("shepherd-target-click-disabled"))}_updateStepTargetOnHide(){let a=this.target||document.body;this.options.highlightClass&&a.classList.remove(this.options.highlightClass);a.classList.remove("shepherd-target-click-disabled",`${this.classPrefix}shepherd-enabled`,`${this.classPrefix}shepherd-target`)}}class Dc extends T{constructor(a){super();S(this,a,yc,xc,Q,{element:0,openingProperties:4,getElement:5,closeModalOpening:6,hide:7,positionModal:8,setupForStep:9,
+show:10})}get getElement(){return this.$$.ctx[5]}get closeModalOpening(){return this.$$.ctx[6]}get hide(){return this.$$.ctx[7]}get positionModal(){return this.$$.ctx[8]}get setupForStep(){return this.$$.ctx[9]}get show(){return this.$$.ctx[10]}}let oa=new Ra;class Ec extends Ra{constructor(a){void 0===a&&(a={});super(a);Va(this);this.options=Object.assign({},{exitOnEsc:!0,keyboardNavigation:!0},a);this.classPrefix=jb(this.options.classPrefix);this.steps=[];this.addSteps(this.options.steps);"active cancel complete inactive show start".split(" ").map(b=>
+{(c=>{this.on(c,d=>{d=d||{};d.tour=this;oa.trigger(c,d)})})(b)});this._setTourID();return this}addStep(a,b){a instanceof Sa?a.tour=this:a=new Sa(this,a);void 0!==b?this.steps.splice(b,0,a):this.steps.push(a);return a}addSteps(a){Array.isArray(a)&&a.forEach(b=>{this.addStep(b)});return this}back(){let a=this.steps.indexOf(this.currentStep);this.show(a-1,!1)}cancel(){this.options.confirmCancel?window.confirm(this.options.confirmCancelMessage||"Are you sure you want to stop the tour?")&&this._done("cancel"):
+this._done("cancel")}complete(){this._done("complete")}getById(a){return this.steps.find(b=>b.id===a)}getCurrentStep(){return this.currentStep}hide(){let a=this.getCurrentStep();if(a)return a.hide()}isActive(){return oa.activeTour===this}next(){let a=this.steps.indexOf(this.currentStep);a===this.steps.length-1?this.complete():this.show(a+1,!0)}removeStep(a){let b=this.getCurrentStep();this.steps.some((c,d)=>{if(c.id===a)return c.isOpen()&&c.hide(),c.destroy(),this.steps.splice(d,1),!0});b&&b.id===
+a&&(this.currentStep=void 0,this.steps.length?this.show(0):this.cancel())}show(a,b){void 0===a&&(a=0);void 0===b&&(b=!0);if(a=qa(a)?this.getById(a):this.steps[a])this._updateStateBeforeShow(),ea(a.options.showOn)&&!a.options.showOn()?this._skipStep(a,b):(this.trigger("show",{step:a,previous:this.currentStep}),this.currentStep=a,a.show())}start(){this.trigger("start");this.focusedElBeforeOpen=document.activeElement;this.currentStep=null;this._setupModal();this._setupActiveTour();this.next()}_done(a){let b=
+this.steps.indexOf(this.currentStep);Array.isArray(this.steps)&&this.steps.forEach(c=>c.destroy());wc(this);this.trigger(a,{index:b});oa.activeTour=null;this.trigger("inactive",{tour:this});this.modal&&this.modal.hide();"cancel"!==a&&"complete"!==a||!this.modal||(a=document.querySelector(".shepherd-modal-overlay-container"))&&a.remove();this.focusedElBeforeOpen instanceof HTMLElement&&this.focusedElBeforeOpen.focus()}_setupActiveTour(){this.trigger("active",{tour:this});oa.activeTour=this}_setupModal(){this.modal=
+new Dc({target:this.options.modalContainer||document.body,props:{classPrefix:this.classPrefix,styles:this.styles}})}_skipStep(a,b){a=this.steps.indexOf(a);a=b?a+1:a-1;a===this.steps.length-1?this.complete():this.show(a,b)}_updateStateBeforeShow(){this.currentStep&&this.currentStep.hide();this.isActive()||this._setupActiveTour()}_setTourID(){this.id=`${this.options.tourName||"tour"}--${Na()}`}}Object.assign(oa,{Tour:Ec,Step:Sa});return oa})

+ 2 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Logseq",
   "name": "Logseq",
-  "version": "0.7.0",
+  "version": "0.7.1",
   "main": "electron.js",
   "main": "electron.js",
   "author": "Logseq",
   "author": "Logseq",
   "license": "AGPL-3.0",
   "license": "AGPL-3.0",
@@ -36,7 +36,7 @@
     "https-proxy-agent": "5.0.0",
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.16",
+    "@logseq/rsapi": "0.0.20",
     "electron-deeplink": "1.0.10"
     "electron-deeplink": "1.0.10"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 1 - 0
scripts/bump-version.sh

@@ -58,6 +58,7 @@ $SED -i 's/defonce version ".*"/defonce version "'${NEW_VERSION}'"/g' src/main/f
 $SED -i 's/"version": ".*"/"version": "'${NEW_VERSION}'"/g' resources/package.json
 $SED -i 's/"version": ".*"/"version": "'${NEW_VERSION}'"/g' resources/package.json
 $SED -i 's/versionName ".*"/versionName "'${NEW_VERSION}'"/g' android/app/build.gradle
 $SED -i 's/versionName ".*"/versionName "'${NEW_VERSION}'"/g' android/app/build.gradle
 $SED -i 's/versionCode .*/versionCode '${NEW_VERSION_CODE}'/g' android/app/build.gradle
 $SED -i 's/versionCode .*/versionCode '${NEW_VERSION_CODE}'/g' android/app/build.gradle
+$SED -i 's/MARKETING_VERSION = .*;/MARKETING_VERSION = '${NEW_VERSION}';/g' ios/App/App.xcodeproj/project.pbxproj
 
 
 git --no-pager diff -U0
 git --no-pager diff -U0
 
 

+ 1 - 1
scripts/carve.clj

@@ -7,7 +7,7 @@
 
 
 (require '[babashka.pods :as pods])
 (require '[babashka.pods :as pods])
 
 
-(pods/load-pod 'clj-kondo/clj-kondo "2021.12.19")
+(pods/load-pod 'clj-kondo/clj-kondo "2022.02.09")
 (require '[pod.borkdude.clj-kondo :as clj-kondo])
 (require '[pod.borkdude.clj-kondo :as clj-kondo])
 ;; define clj-kondo.core ns which is used by carve
 ;; define clj-kondo.core ns which is used by carve
 (intern (create-ns 'clj-kondo.core) 'run! clj-kondo/run!)
 (intern (create-ns 'clj-kondo.core) 'run! clj-kondo/run!)

+ 9 - 5
scripts/large_vars.clj

@@ -5,12 +5,13 @@
   the team to maintain and understand them."
   the team to maintain and understand them."
   (:require [babashka.pods :as pods]
   (:require [babashka.pods :as pods]
             [clojure.pprint :as pprint]
             [clojure.pprint :as pprint]
+            [clojure.edn :as edn]
             [clojure.set :as set]))
             [clojure.set :as set]))
 
 
-(pods/load-pod 'clj-kondo/clj-kondo "2021.12.19")
+(pods/load-pod 'clj-kondo/clj-kondo "2022.02.09")
 (require '[pod.borkdude.clj-kondo :as clj-kondo])
 (require '[pod.borkdude.clj-kondo :as clj-kondo])
 
 
-(def config
+(def default-config
   ;; TODO: Discuss with team and agree on lower number
   ;; TODO: Discuss with team and agree on lower number
   {:max-lines-count 100
   {:max-lines-count 100
    ;; Vars with these metadata flags are allowed. Name should indicate the reason
    ;; Vars with these metadata flags are allowed. Name should indicate the reason
@@ -23,11 +24,14 @@
 
 
 (defn -main
 (defn -main
   [args]
   [args]
-  (let [paths (or args ["src"])
+  (let [paths [(or (first args) "src")]
+        config (or (some->> (second args) edn/read-string (merge default-config))
+                   default-config)
         {{:keys [var-definitions]} :analysis}
         {{:keys [var-definitions]} :analysis}
         (clj-kondo/run!
         (clj-kondo/run!
          {:lint paths
          {:lint paths
-          :config {:output {:analysis {:var-definitions {:meta true}}}}})
+          :config {:output {:analysis {:var-definitions {:meta true
+                                                         :lang :cljs}}}}})
         vars (->> var-definitions
         vars (->> var-definitions
                   (keep (fn [m]
                   (keep (fn [m]
                           (let [lines-count (inc (- (:end-row m) (:row m)))]
                           (let [lines-count (inc (- (:end-row m) (:row m)))]
@@ -40,7 +44,7 @@
                   (sort-by :lines-count (fn [x y] (compare y x))))]
                   (sort-by :lines-count (fn [x y] (compare y x))))]
     (if (seq vars)
     (if (seq vars)
       (do
       (do
-        (println (format "The following vars exceed the line count max of %s:"
+        (println (format "\nThe following vars exceed the line count max of %s:"
                          (:max-lines-count config)))
                          (:max-lines-count config)))
         (pprint/print-table vars)
         (pprint/print-table vars)
         (System/exit 1))
         (System/exit 1))

+ 1 - 1
scripts/src/logseq/tasks/dev.clj

@@ -34,7 +34,7 @@
   - lint invalid translation entries
   - lint invalid translation entries
   - Lint datalog rules"
   - Lint datalog rules"
   []
   []
-  (doseq [cmd ["clojure -M:clj-kondo --parallel --lint src"
+  (doseq [cmd ["clojure -M:clj-kondo --parallel --lint src --cache false"
                "scripts/carve.clj"
                "scripts/carve.clj"
                "scripts/large_vars.clj"
                "scripts/large_vars.clj"
                "bb lang:invalid-translations"
                "bb lang:invalid-translations"

+ 25 - 8
scripts/src/logseq/tasks/nbb.clj

@@ -1,15 +1,16 @@
 (ns logseq.tasks.nbb
 (ns logseq.tasks.nbb
   (:require [pod.borkdude.clj-kondo :as clj-kondo]
   (:require [pod.borkdude.clj-kondo :as clj-kondo]
+            [clojure.string :as str]
             [babashka.tasks :refer [shell]]))
             [babashka.tasks :refer [shell]]))
 
 
 (defn- fetch-meta-namespaces
 (defn- fetch-meta-namespaces
   "Return namespaces with metadata"
   "Return namespaces with metadata"
   [paths]
   [paths]
-  (let [paths (or (seq paths) ["src"])
-        {{:keys [namespace-definitions]} :analysis}
+  (let [{{:keys [namespace-definitions]} :analysis}
         (clj-kondo/run!
         (clj-kondo/run!
          {:lint paths
          {:lint paths
-          :config {:output {:analysis {:namespace-definitions {:meta true}}}}})
+          :config {:output {:analysis {:namespace-definitions {:meta true
+                                                               :lang :cljs}}}}})
         matches (keep (fn [m]
         matches (keep (fn [m]
                         (when (:meta m)
                         (when (:meta m)
                           {:ns   (:name m)
                           {:ns   (:name m)
@@ -17,14 +18,30 @@
                       namespace-definitions)]
                       namespace-definitions)]
     matches))
     matches))
 
 
+(defn- validate-namespaces
+  [namespaces classpath dir]
+  (assert (seq namespaces) "There must be some namespaces to check")
+  ;; distinct b/c sometimes namespaces are duplicated with .cljc analysis
+  (doseq [n (distinct namespaces)]
+    (println "Requiring" n "...")
+    (shell {:dir dir} "yarn nbb-logseq -cp" classpath "-e" (format "(require '[%s])" n)))
+  (println "Success!"))
+
 (defn load-compatible-namespaces
 (defn load-compatible-namespaces
   "Check nbb-compatible namespaces can be required by nbb-logseq"
   "Check nbb-compatible namespaces can be required by nbb-logseq"
   []
   []
   (let [namespaces (map :ns
   (let [namespaces (map :ns
                         (filter #(get-in % [:meta :nbb-compatible])
                         (filter #(get-in % [:meta :nbb-compatible])
                                 (fetch-meta-namespaces ["src/main"])))]
                                 (fetch-meta-namespaces ["src/main"])))]
-    (assert (seq namespaces) "There must be some nbb namespaces to check")
-    (doseq [n namespaces]
-      (println "Requiring" n "...")
-      (shell "yarn nbb-logseq -cp src/main -e" (format "(require '[%s])" n)))
-    (println "Success!")))
+    (validate-namespaces namespaces "src/main" ".")))
+
+(defn load-all-namespaces
+  "Check all namespaces in source path(s) can be required by nbb-logseq"
+  [dir & paths]
+  (let [{{:keys [namespace-definitions]} :analysis}
+        (clj-kondo/run!
+         {:lint (map #(str dir "/" %) paths)
+          :config {:output {:analysis {:namespace-definitions {:lang :cljs}}}}})]
+    (validate-namespaces (map :name namespace-definitions)
+                         (str/join ":" paths)
+                         dir)))

+ 66 - 19
src/electron/electron/core.cljs

@@ -1,7 +1,7 @@
 (ns electron.core
 (ns electron.core
   (:require [electron.handler :as handler]
   (:require [electron.handler :as handler]
             [electron.search :as search]
             [electron.search :as search]
-            [electron.updater :refer [init-updater]]
+            [electron.updater :refer [init-updater] :as updater]
             [electron.utils :refer [*win mac? linux? dev? logger get-win-from-sender restore-user-fetch-agent]]
             [electron.utils :refer [*win mac? linux? dev? logger get-win-from-sender restore-user-fetch-agent]]
             [electron.url :refer [logseq-url-handler]]
             [electron.url :refer [logseq-url-handler]]
             [clojure.string :as string]
             [clojure.string :as string]
@@ -11,7 +11,7 @@
             ["fs-extra" :as fs]
             ["fs-extra" :as fs]
             ["path" :as path]
             ["path" :as path]
             ["os" :as os]
             ["os" :as os]
-            ["electron" :refer [BrowserWindow app protocol ipcMain dialog] :as electron]
+            ["electron" :refer [BrowserWindow Menu app protocol ipcMain dialog shell] :as electron]
             ["electron-deeplink" :refer [Deeplink]]
             ["electron-deeplink" :refer [Deeplink]]
             [clojure.core.async :as async]
             [clojure.core.async :as async]
             [electron.state :as state]
             [electron.state :as state]
@@ -81,7 +81,7 @@
      (.unregisterProtocol protocol FILE_LSP_SCHEME)
      (.unregisterProtocol protocol FILE_LSP_SCHEME)
      (.unregisterProtocol protocol "assets")))
      (.unregisterProtocol protocol "assets")))
 
 
-(defn- handle-export-publish-assets [_event html custom-css-path repo-path asset-filenames output-path]
+(defn- handle-export-publish-assets [_event html custom-css-path export-css-path repo-path asset-filenames output-path]
   (p/let [app-path (. app getAppPath)
   (p/let [app-path (. app getAppPath)
           asset-filenames (js->clj asset-filenames)
           asset-filenames (js->clj asset-filenames)
           root-dir (or output-path (handler/open-dir-dialog))]
           root-dir (or output-path (handler/open-dir-dialog))]
@@ -89,7 +89,8 @@
       (let [static-dir (path/join root-dir "static")
       (let [static-dir (path/join root-dir "static")
             assets-from-dir (path/join repo-path "assets")
             assets-from-dir (path/join repo-path "assets")
             assets-to-dir (path/join root-dir "assets")
             assets-to-dir (path/join root-dir "assets")
-            index-html-path (path/join root-dir "index.html")]
+            index-html-path (path/join root-dir "index.html")
+            export-or-custom-css-path (if (fs/existsSync export-css-path) export-css-path custom-css-path)]
         (p/let [_ (. fs ensureDir static-dir)
         (p/let [_ (. fs ensureDir static-dir)
                 _ (. fs ensureDir assets-to-dir)
                 _ (. fs ensureDir assets-to-dir)
                 _ (p/all (concat
                 _ (p/all (concat
@@ -99,35 +100,35 @@
                            (. fs copy (path/join app-path "404.html") (path/join root-dir "404.html"))]
                            (. fs copy (path/join app-path "404.html") (path/join root-dir "404.html"))]
 
 
                           (map
                           (map
-                            (fn [filename]
-                              (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
-                                  (p/catch
-                                      (fn [e]
-                                        (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
-                                        (js/console.error e)))))
-                            asset-filenames)
+                           (fn [filename]
+                             (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
+                                 (p/catch
+                                  (fn [e]
+                                    (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
+                                    (js/console.error e)))))
+                           asset-filenames)
 
 
                           (map
                           (map
-                            (fn [part]
-                              (. fs copy (path/join app-path part) (path/join static-dir part)))
-                            ["css" "fonts" "icons" "img" "js"])))
-                custom-css (. fs readFile custom-css-path)
-                _ (. fs writeFile (path/join static-dir "css" "custom.css") custom-css)
+                           (fn [part]
+                             (. fs copy (path/join app-path part) (path/join static-dir part)))
+                           ["css" "fonts" "icons" "img" "js"])))
+                export-css (. fs readFile export-or-custom-css-path)
+                _ (. fs writeFile (path/join static-dir "css" "export.css") export-css)
                 js-files ["main.js" "code-editor.js" "excalidraw.js"]
                 js-files ["main.js" "code-editor.js" "excalidraw.js"]
                 _ (p/all (map (fn [file]
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" file)))
                                 (. fs removeSync (path/join static-dir "js" file)))
-                           js-files))
+                              js-files))
                 _ (p/all (map (fn [file]
                 _ (p/all (map (fn [file]
                                 (. fs moveSync
                                 (. fs moveSync
                                    (path/join static-dir "js" "publishing" file)
                                    (path/join static-dir "js" "publishing" file)
                                    (path/join static-dir "js" file)))
                                    (path/join static-dir "js" file)))
-                           js-files))
+                              js-files))
                 _ (. fs removeSync (path/join static-dir "js" "publishing"))
                 _ (. fs removeSync (path/join static-dir "js" "publishing"))
                 ;; remove source map files
                 ;; remove source map files
                 ;; TODO: ugly, replace with ls-files and filter with ".map"
                 ;; TODO: ugly, replace with ls-files and filter with ".map"
                 _ (p/all (map (fn [file]
                 _ (p/all (map (fn [file]
                                 (. fs removeSync (path/join static-dir "js" (str file ".map"))))
                                 (. fs removeSync (path/join static-dir "js" (str file ".map"))))
-                           ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
+                              ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
           (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
           (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
 
 
 (defn setup-app-manager!
 (defn setup-app-manager!
@@ -179,6 +180,51 @@
          (.removeHandler ipcMain call-app-channel)
          (.removeHandler ipcMain call-app-channel)
          (.removeHandler ipcMain call-win-channel))))
          (.removeHandler ipcMain call-win-channel))))
 
 
+(defn- set-app-menu! []
+  (let [about-fn (fn []
+                   (.showMessageBox dialog (clj->js {:title "Logseq"
+                                                     :icon (path/join js/__dirname "icons/logseq.png")
+                                                     :message (str "Version " updater/electron-version)})))
+        template (if mac?
+                   [{:label (.-name app)
+                     :submenu [{:role "about"}
+                               {:type "separator"}
+                               {:role "services"}
+                               {:type "separator"}
+                               {:role "hide"}
+                               {:role "hideOthers"}
+                               {:role "unhide"}
+                               {:type "separator"}
+                               {:role "quit"}]}]
+                   [])
+        template (conj template
+                       {:role "fileMenu"
+                        :submenu [{:label "New Window"
+                                   :click (fn []
+                                            (handler/open-new-window!))
+                                   :accelerator "CommandOrControl+N"
+                                   :acceleratorWorksWhenHidden false}
+                                  (if mac?
+                                    {:role "close"}
+                                    {:role "quit"})]}
+                       {:role "editMenu"}
+                       {:role "viewMenu"}
+                       {:role "windowMenu"})
+        ;; Windows has no about role
+        template (conj template
+                       (if mac?
+                         {:role "help"
+                          :submenu [{:label "Official Documentation"
+                                     :click #(.openExternal shell "https://docs.logseq.com/")}]}
+                         {:role "help"
+                          :submenu [{:label "Official Documentation"
+                                     :click #(.openExternal shell "https://docs.logseq.com/")}
+                                    {:role "about"
+                                     :label "About Logseq"
+                                     :click about-fn}]}))
+        menu (.buildFromTemplate Menu (clj->js template))]
+    (.setApplicationMenu Menu menu)))
+
 (defn- setup-deeplink! []
 (defn- setup-deeplink! []
   ;; Works for Deeplink v1.0.9
   ;; Works for Deeplink v1.0.9
   ;; :mainWindow is only used for handeling window restoring on second-instance,
   ;; :mainWindow is only used for handeling window restoring on second-instance,
@@ -209,6 +255,7 @@
                              {:scheme     FILE_LSP_SCHEME
                              {:scheme     FILE_LSP_SCHEME
                               :privileges privileges}]))
                               :privileges privileges}]))
 
 
+      (set-app-menu!)
       (setup-deeplink!)
       (setup-deeplink!)
 
 
       (.on app "second-instance"
       (.on app "second-instance"

+ 41 - 22
src/electron/electron/fs_watcher.cljs

@@ -4,7 +4,8 @@
             ["chokidar" :as watcher]
             ["chokidar" :as watcher]
             [electron.utils :as utils]
             [electron.utils :as utils]
             ["electron" :refer [app]]
             ["electron" :refer [app]]
-            [electron.window :as window]))
+            [electron.window :as window]
+            ["path" :as path]))
 
 
 ;; TODO: explore different solutions for different platforms
 ;; TODO: explore different solutions for different platforms
 ;; 1. https://github.com/Axosoft/nsfw
 ;; 1. https://github.com/Axosoft/nsfw
@@ -29,10 +30,13 @@
 
 
 (defn- publish-file-event!
 (defn- publish-file-event!
   [dir path event]
   [dir path event]
-  (let [content (when (and (not= event "unlink")
+  (let [dir-path? (= dir path)
+        content (when (and (not= event "unlink")
+                           (not dir-path?)
                            (utils/should-read-content? path))
                            (utils/should-read-content? path))
                   (utils/read-file path))
                   (utils/read-file path))
-        stat (when (not= event "unlink")
+        stat (when (and (not= event "unlink")
+                        (not dir-path?))
                (fs/statSync path))]
                (fs/statSync path))]
     (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
     (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
                                    :path (utils/fix-win-path! path)
                                    :path (utils/fix-win-path! path)
@@ -44,38 +48,53 @@
   [_win dir]
   [_win dir]
   (when (and (fs/existsSync dir)
   (when (and (fs/existsSync dir)
              (not (get @*file-watcher dir)))
              (not (get @*file-watcher dir)))
-    (let [watcher (.watch watcher dir
-                          (clj->js
-                           {:ignored (fn [path]
-                                       (utils/ignored-path? dir path))
-                            :ignoreInitial false
-                            :ignorePermissionErrors true
-                            :interval polling-interval
-                            :binaryInterval polling-interval
-                            :persistent true
-                            :disableGlobbing true
-                            :usePolling false
-                            :awaitWriteFinish true}))
-          watcher-del-f #(.close watcher)]
-      (swap! *file-watcher assoc dir [watcher watcher-del-f])
+    (let [watcher-opts (clj->js
+                        {:ignored (fn [path]
+                                    (utils/ignored-path? dir path))
+                         :ignoreInitial false
+                         :ignorePermissionErrors true
+                         :interval polling-interval
+                         :binaryInterval polling-interval
+                         :persistent true
+                         :disableGlobbing true
+                         :usePolling false
+                         :awaitWriteFinish true})
+          dir-watcher (.watch watcher dir watcher-opts)
+          watcher-del-f #(.close dir-watcher)
+          parent-dir (path/join dir "..")
+          parent-watcher (.watch watcher parent-dir watcher-opts)
+          parent-watcher-del-f #(.close parent-watcher)]
+      (swap! *file-watcher assoc dir
+             [dir-watcher watcher-del-f]
+             [parent-watcher parent-watcher-del-f])
       ;; TODO: batch sender
       ;; TODO: batch sender
-      (.on watcher "add"
+      (.on parent-watcher "unlinkDir"
+           (fn [path]
+             (when (= dir path)
+               (publish-file-event! dir dir "unlinkDir"))))
+      (.on parent-watcher "addDir"
+           (fn [path]
+             (when (= dir path)
+               (publish-file-event! dir dir "addDir"))))
+      (.on dir-watcher "add"
            (fn [path]
            (fn [path]
              (publish-file-event! dir path "add")))
              (publish-file-event! dir path "add")))
-      (.on watcher "change"
+      (.on dir-watcher "change"
            (fn [path]
            (fn [path]
              (publish-file-event! dir path "change")))
              (publish-file-event! dir path "change")))
-      (.on watcher "unlink"
+      (.on dir-watcher "unlink"
            (fn [path]
            (fn [path]
              (publish-file-event! dir path "unlink")))
              (publish-file-event! dir path "unlink")))
-      (.on watcher "error"
+      (.on dir-watcher "error"
            (fn [path]
            (fn [path]
              (println "Watch error happened: "
              (println "Watch error happened: "
                       {:path path})))
                       {:path path})))
 
 
       ;; electron app extends `EventEmitter`
       ;; electron app extends `EventEmitter`
       ;; TODO check: duplicated with the logic in "window-all-closed" ?
       ;; TODO check: duplicated with the logic in "window-all-closed" ?
-      (.on app "quit" watcher-del-f)
+      (.on app "quit" (fn []
+                        (watcher-del-f)
+                        (parent-watcher-del-f)))
 
 
       true)))
       true)))
 
 

+ 85 - 29
src/main/frontend/components/block.cljs

@@ -12,35 +12,37 @@
             [frontend.commands :as commands]
             [frontend.commands :as commands]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.lazy-editor :as lazy-editor]
-            [frontend.components.svg :as svg]
             [frontend.components.macro :as macro]
             [frontend.components.macro :as macro]
+            [frontend.components.plugins :as plugins]
+            [frontend.components.query-table :as query-table]
+            [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
-            [frontend.db.utils :as db-utils]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-dsl :as query-dsl]
+            [frontend.db.utils :as db-utils]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.latex :as latex]
             [frontend.extensions.latex :as latex]
-            [frontend.extensions.sci :as sci]
-            [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.extensions.zotero :as zotero]
             [frontend.extensions.lightbox :as lightbox]
             [frontend.extensions.lightbox :as lightbox]
+            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.sci :as sci]
             [frontend.extensions.video.youtube :as youtube]
             [frontend.extensions.video.youtube :as youtube]
+            [frontend.extensions.zotero :as zotero]
             [frontend.format.block :as block]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.mldoc :as mldoc]
-            [frontend.components.plugins :as plugins]
-            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
+            [frontend.handler.common :as common-handler]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.dnd :as dnd]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.plugin :as plugin-handler]
+            [frontend.handler.query :as query-handler]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.handler.query :as query-handler]
-            [frontend.handler.common :as common-handler]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.tree :as tree]
             [frontend.search :as search]
             [frontend.search :as search]
             [frontend.security :as security]
             [frontend.security :as security]
@@ -50,8 +52,9 @@
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.clock :as clock]
-            [frontend.util.property :as property]
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
+            [frontend.util.text :as text-util]
+            [frontend.util.property :as property]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
@@ -63,9 +66,7 @@
             [promesa.core :as p]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [rum.core :as rum]
-            [shadow.loader :as loader]
-            [frontend.components.query-table :as query-table]
-            [frontend.mobile.util :as mobile-util]))
+            [shadow.loader :as loader]))
 
 
 (defn safe-read-string
 (defn safe-read-string
   ([s]
   ([s]
@@ -256,7 +257,8 @@
 
 
 (rum/defc audio-cp [src]
 (rum/defc audio-cp [src]
   [:audio {:src src
   [:audio {:src src
-           :controls true}])
+           :controls true
+           :on-touch-start #(util/stop %)}])
 
 
 (rum/defcs asset-link < rum/reactive
 (rum/defcs asset-link < rum/reactive
   (rum/local nil ::src)
   (rum/local nil ::src)
@@ -279,7 +281,7 @@
           (contains? config/audio-formats ext)
           (contains? config/audio-formats ext)
           (audio-cp @src)
           (audio-cp @src)
 
 
-          (contains? (config/img-formats) ext)
+          (contains? (gp-config/img-formats) ext)
           (resizable-image config title @src metadata full_text true)
           (resizable-image config title @src metadata full_text true)
 
 
           (= ext :pdf)
           (= ext :pdf)
@@ -827,7 +829,7 @@
         (nil? metadata-show)
         (nil? metadata-show)
         (or
         (or
          (gp-config/local-asset? s)
          (gp-config/local-asset? s)
-         (text/media-link? media-formats s)))
+         (text-util/media-link? media-formats s)))
        (true? (boolean metadata-show))))
        (true? (boolean metadata-show))))
 
 
      ;; markdown
      ;; markdown
@@ -836,7 +838,7 @@
      ;; image http link
      ;; image http link
      (and (or (string/starts-with? full-text "http://")
      (and (or (string/starts-with? full-text "http://")
               (string/starts-with? full-text "https://"))
               (string/starts-with? full-text "https://"))
-          (text/media-link? media-formats s)))))
+          (text-util/media-link? media-formats s)))))
 
 
 (defn- relative-assets-path->absolute-path
 (defn- relative-assets-path->absolute-path
   [path]
   [path]
@@ -1116,7 +1118,7 @@
 (defn- macro-vimeo-cp
 (defn- macro-vimeo-cp
   [_config arguments]
   [_config arguments]
   (when-let [url (first arguments)]
   (when-let [url (first arguments)]
-    (when-let [vimeo-id (nth (util/safe-re-find text/vimeo-regex url) 5)]
+    (when-let [vimeo-id (nth (util/safe-re-find text-util/vimeo-regex url) 5)]
       (when-not (string/blank? vimeo-id)
       (when-not (string/blank? vimeo-id)
         (let [width (min (- (util/get-width) 96)
         (let [width (min (- (util/get-width) 96)
                          560)
                          560)
@@ -1136,7 +1138,7 @@
     (when-let [id (cond
     (when-let [id (cond
                     (<= (count url) 15) url
                     (<= (count url) 15) url
                     :else
                     :else
-                    (nth (util/safe-re-find text/bilibili-regex url) 5))]
+                    (nth (util/safe-re-find text-util/bilibili-regex url) 5))]
       (when-not (string/blank? id)
       (when-not (string/blank? id)
         (let [width (min (- (util/get-width) 96)
         (let [width (min (- (util/get-width) 96)
                          560)
                          560)
@@ -1157,7 +1159,7 @@
     (let [width (min (- (util/get-width) 96)
     (let [width (min (- (util/get-width) 96)
                      560)
                      560)
           height (int (* width (/ 315 560)))
           height (int (* width (/ 315 560)))
-          results (text/get-matched-video url)
+          results (text-util/get-matched-video url)
           src (match results
           src (match results
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
                      (if (= (count id) 11) ["youtube-player" id] url)
                      (if (= (count id) 11) ["youtube-player" id] url)
@@ -1305,7 +1307,7 @@
         (when-let [youtube-id (cond
         (when-let [youtube-id (cond
                                 (== 11 (count url)) url
                                 (== 11 (count url)) url
                                 :else
                                 :else
-                                (nth (util/safe-re-find text/youtube-regex url) 5))]
+                                (nth (util/safe-re-find text-util/youtube-regex url) 5))]
           (when-not (string/blank? youtube-id)
           (when-not (string/blank? youtube-id)
             (youtube/youtube-video youtube-id))))
             (youtube/youtube-video youtube-id))))
 
 
@@ -1796,6 +1798,9 @@
        (int? v)
        (int? v)
        v
        v
 
 
+       (= k :file-path)
+       v
+
        date
        date
        date
        date
 
 
@@ -2087,13 +2092,34 @@
                        (swap! *hide-block-refs? not)))}
                        (swap! *hide-block-refs? not)))}
         block-refs-count]])))
         block-refs-count]])))
 
 
+(rum/defc block-left-menu < rum/reactive
+  [_config {:block/keys [uuid] :as _block}]
+  [:div.block-left-menu.flex.bg-base-2.rounded-r-md.mr-1
+   [:div.commands-button.w-0.rounded-r-md
+    {:id (str "block-left-menu-" uuid)}
+    [:div.indent (ui/icon "indent-increase" {:style {:fontSize 16}})]]])
+
+(rum/defc block-right-menu < rum/reactive
+  [_config {:block/keys [uuid] :as _block} edit?]
+  [:div.block-right-menu.flex.bg-base-2.rounded-md.ml-1
+   [:div.commands-button.w-0.flex.flew-col.rounded-md
+    {:id (str "block-right-menu-" uuid)
+     :style {:max-width (if edit? 40 80)}}
+    [:div.outdent (ui/icon "indent-decrease" {:style {:fontSize 16}})]
+    (when-not edit?
+      [:div.more (ui/icon "dots-circle-horizontal" {:style {:fontSize 16}})])]])
+
 (rum/defcs block-content-or-editor < rum/reactive
 (rum/defcs block-content-or-editor < rum/reactive
   (rum/local true :hide-block-refs?)
   (rum/local true :hide-block-refs?)
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?]
   [state config {:block/keys [uuid format] :as block} edit-input-id block-id heading-level edit?]
   (let [*hide-block-refs? (get state :hide-block-refs?)
   (let [*hide-block-refs? (get state :hide-block-refs?)
         editor-box (get config :editor-box)
         editor-box (get config :editor-box)
         editor-id (str "editor-" edit-input-id)
         editor-id (str "editor-" edit-input-id)
-        slide? (:slide? config)]
+        slide? (:slide? config)
+        trimmed-content (string/trim (:block/content block))
+        block-reference-only? (and (string/starts-with? trimmed-content "((")
+                                   (re-find (re-pattern util/uuid-pattern) trimmed-content)
+                                   (string/ends-with? trimmed-content "))"))]
     (if (and edit? editor-box)
     (if (and edit? editor-box)
       [:div.editor-wrapper {:id editor-id}
       [:div.editor-wrapper {:id editor-id}
        (ui/catch-error
        (ui/catch-error
@@ -2123,13 +2149,20 @@
           [:div.flex.flex-row.items-center
           [:div.flex.flex-row.items-center
            (when (and (:embed? config)
            (when (and (:embed? config)
                       (:embed-parent config))
                       (:embed-parent config))
-             [:a.opacity-30.hover:opacity-100.svg-small.inline
+             [:a.opacity-70.hover:opacity-100.svg-small.inline
               {:on-mouse-down (fn [e]
               {:on-mouse-down (fn [e]
                                 (util/stop e)
                                 (util/stop e)
                                 (when-let [block (:embed-parent config)]
                                 (when-let [block (:embed-parent config)]
                                   (editor-handler/edit-block! block :max (:block/uuid block))))}
                                   (editor-handler/edit-block! block :max (:block/uuid block))))}
               svg/edit])
               svg/edit])
 
 
+           (when block-reference-only?
+             [:a.opacity-70.hover:opacity-100.svg-small.inline
+              {:on-mouse-down (fn [e]
+                                (util/stop e)
+                                (editor-handler/edit-block! block :max (:block/uuid block)))}
+              svg/edit])
+
            (block-refs-count block *hide-block-refs?)]]
            (block-refs-count block *hide-block-refs?)]]
 
 
          (when (and (not @*hide-block-refs?) (> refs-count 0))
          (when (and (not @*hide-block-refs?) (> refs-count 0))
@@ -2321,7 +2354,7 @@
   (let [refs (model/get-page-names-by-ids
   (let [refs (model/get-page-names-by-ids
               (->> (map :db/id refs)
               (->> (map :db/id refs)
                    (remove nil?)))]
                    (remove nil?)))]
-    (text/build-data-value refs)))
+    (text-util/build-data-value refs)))
 
 
 (defn- get-children-refs
 (defn- get-children-refs
   [children]
   [children]
@@ -2382,6 +2415,8 @@
                      (model/sub-block-direct-children repo uuid))
                      (model/sub-block-direct-children repo uuid))
                    children)
                    children)
         breadcrumb-show? (:breadcrumb-show? config)
         breadcrumb-show? (:breadcrumb-show? config)
+        *show-left-menu? (::show-block-left-menu? state)
+        *show-right-menu? (::show-block-right-menu? state)
         slide? (boolean (:slide? config))
         slide? (boolean (:slide? config))
         doc-mode? (:document/mode? config)
         doc-mode? (:document/mode? config)
         embed? (:embed? config)
         embed? (:embed? config)
@@ -2404,7 +2439,7 @@
         :data-collapsed (and collapsed? has-child?)
         :data-collapsed (and collapsed? has-child?)
         :class (str uuid
         :class (str uuid
                     (when pre-block? " pre-block")
                     (when pre-block? " pre-block")
-                    (when (and card? (not review-cards?)) " shadow-xl")
+                    (when (and card? (not review-cards?)) " shadow-md")
                     (when (:ui/selected? block) " selected noselect"))
                     (when (:ui/selected? block) " selected noselect"))
         :blockid (str uuid)
         :blockid (str uuid)
         :haschild (str has-child?)}
         :haschild (str has-child?)}
@@ -2435,6 +2470,12 @@
 
 
      [:div.flex.flex-row.pr-2
      [:div.flex.flex-row.pr-2
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
       {:class (if (and heading? (seq (:block/title block))) "items-baseline" "")
+       :on-touch-start (fn [event uuid] (block-handler/on-touch-start event uuid))
+       :on-touch-move (fn [event]
+                        (block-handler/on-touch-move event block uuid edit? *show-left-menu? *show-right-menu?))
+       :on-touch-end (fn [event]
+                       (block-handler/on-touch-end event block uuid *show-left-menu? *show-right-menu?))
+       :on-touch-cancel block-handler/on-touch-cancel
        :on-mouse-over (fn [e]
        :on-mouse-over (fn [e]
                         (block-mouse-over uuid e *control-show? block-id doc-mode?))
                         (block-mouse-over uuid e *control-show? block-id doc-mode?))
        :on-mouse-leave (fn [e]
        :on-mouse-leave (fn [e]
@@ -2442,13 +2483,19 @@
       (when (not slide?)
       (when (not slide?)
         (block-control config block uuid block-id collapsed? *control-show? edit?))
         (block-control config block uuid block-id collapsed? *control-show? edit?))
 
 
-      (block-content-or-editor config block edit-input-id block-id heading-level edit?)]
+      (when @*show-left-menu?
+        (block-left-menu config block))
+      (block-content-or-editor config block edit-input-id block-id heading-level edit?)
+      (when @*show-right-menu?
+        (block-right-menu config block edit?))]
 
 
      (block-children config children collapsed?)
      (block-children config children collapsed?)
 
 
      (dnd-separator-wrapper block block-id slide? false false)]))
      (dnd-separator-wrapper block block-id slide? false false)]))
 
 
 (rum/defcs block-container < rum/reactive
 (rum/defcs block-container < rum/reactive
+  (rum/local false ::show-block-left-menu?)
+  (rum/local false ::show-block-right-menu?)
   {:init (fn [state]
   {:init (fn [state]
            (let [[config block] (:rum/args state)
            (let [[config block] (:rum/args state)
                  block-id (:block/uuid block)]
                  block-id (:block/uuid block)]
@@ -2696,6 +2743,8 @@
   [state config {:keys [title query view collapsed? children? breadcrumb-show? table-view?] :as q}]
   [state config {:keys [title query view collapsed? children? breadcrumb-show? table-view?] :as q}]
   (let [dsl-query? (:dsl-query? config)
   (let [dsl-query? (:dsl-query? config)
         query-atom (:query-atom state)
         query-atom (:query-atom state)
+        repo (state/get-current-repo)
+        view-fn (if (keyword? view) (state/sub [:config repo :query/views view]) view)
         current-block-uuid (or (:block/uuid (:block config))
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
                                (:block/uuid config))
         current-block (db/entity [:block/uuid current-block-uuid])
         current-block (db/entity [:block/uuid current-block-uuid])
@@ -2716,7 +2765,7 @@
         _ (when-let [query-result (:query-result config)]
         _ (when-let [query-result (:query-result config)]
             (let [result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)]
             (let [result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)]
               (reset! query-result result)))
               (reset! query-result result)))
-        view-f (and view (sci/eval-string (pr-str view)))
+        view-f (and view-fn (sci/eval-string (pr-str view-fn)))
         only-blocks? (:block/uuid (first result))
         only-blocks? (:block/uuid (first result))
         blocks-grouped-by-page? (and (seq result)
         blocks-grouped-by-page? (and (seq result)
                                      (not not-grouped-by-page?)
                                      (not not-grouped-by-page?)
@@ -2735,8 +2784,8 @@
       (when-not (and built-in? (empty? result))
       (when-not (and built-in? (empty? result))
         [:div.custom-query.mt-4 (get config :attr {})
         [:div.custom-query.mt-4 (get config :attr {})
          (ui/foldable
          (ui/foldable
-          [:div.custom-query-title
-           [:span.title-text (cond
+          [:div.custom-query-title.flex.justify-between.w-full
+           [:div [:span.title-text (cond
                                (vector? title) title
                                (vector? title) title
                                (string? title) (inline-text config
                                (string? title) (inline-text config
                                                             (get-in config [:block :block/format] :markdown)
                                                             (get-in config [:block :block/format] :markdown)
@@ -2744,6 +2793,13 @@
                                :else title)]
                                :else title)]
            [:span.opacity-60.text-sm.ml-2.results-count
            [:span.opacity-60.text-sm.ml-2.results-count
             (str (count transformed-query-result) " results")]]
             (str (count transformed-query-result) " results")]]
+           ;;insert an "edit" button in the query view
+           [:a.opacity-70.hover:opacity-100.svg-small.inline 
+            {:on-mouse-down (fn [e]
+                              (util/stop e)
+                              (editor-handler/edit-block! current-block :max (:block/uuid current-block)))}
+            svg/edit]]
+          
           (fn []
           (fn []
             [:div
             [:div
              (when (and current-block (not view-f) (nil? table-view?))
              (when (and current-block (not view-f) (nil? table-view?))

+ 39 - 1
src/main/frontend/components/block.css

@@ -185,6 +185,43 @@
   }
   }
 }
 }
 
 
+.block-left-menu {
+    background-color: var(--ls-secondary-background-color);
+    background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
+    
+    .commands-button {
+        overflow: hidden;
+        max-width: 40px;
+        text-align: center;
+        margin: auto 0;
+
+        .indent {
+            opacity: 30%;
+        }
+    }
+}
+
+.block-right-menu {
+    background-color: var(--ls-secondary-background-color);
+    /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
+
+    .commands-button {
+        overflow: hidden;
+        text-align: center;
+        margin: auto 0;
+
+        .outdent {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+
+        .more {
+            margin: 0 12px;
+            opacity: 30%;
+        }
+    }
+}
+
 .block-ref {
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
   border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -586,7 +623,8 @@ a.cloze-revealed {
 
 
 html.is-native-ios {
 html.is-native-ios {
     audio {
     audio {
-        width: 300px;
+        width: 100%;
+        max-width: 350px;
     }
     }
 }
 }
 
 

+ 5 - 11
src/main/frontend/components/editor.cljs

@@ -23,8 +23,8 @@
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [promesa.core :as p]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.mobile.footer :as footer]))
+            [react-draggable]
+            [rum.core :as rum]))
 
 
 (rum/defc commands < rum/reactive
 (rum/defc commands < rum/reactive
   [id format]
   [id format]
@@ -281,7 +281,6 @@
         max-width 300
         max-width 300
         offset-top 24
         offset-top 24
         vw-height js/window.innerHeight
         vw-height js/window.innerHeight
-        vw-width js/window.innerWidth
         to-max-height (if (and (seq rect) (> vw-height max-height))
         to-max-height (if (and (seq rect) (> vw-height max-height))
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                         (let [delta-height (- vw-height (+ (:top rect) top offset-top))]
                           (if (< delta-height max-height)
                           (if (< delta-height max-height)
@@ -301,15 +300,14 @@
                                    (when (> ofx 0)
                                    (when (> ofx 0)
                                      (set! (.-transform (.-style el)) (str "translateX(-" (+ ofx 20) "px)")))))))
                                      (set! (.-transform (.-style el)) (str "translateX(-" (+ ofx 20) "px)")))))))
                            [right-sidebar? editing-key])
                            [right-sidebar? editing-key])
-        x-overflow-vw? (when (and (seq rect) (> vw-width max-width))
-                         (let [delta-width (- vw-width (+ (:left rect) left))]
-                           (< delta-width (* max-width 0.5))))
+        y-overflow-vh? (< to-max-height 130)
+        to-max-height (if y-overflow-vh? max-height to-max-height)
         pos-rect (when (and (seq rect) editing-key)
         pos-rect (when (and (seq rect) editing-key)
                    (:rect (cursor/get-caret-pos (state/get-input))))
                    (:rect (cursor/get-caret-pos (state/get-input))))
         y-diff (when pos-rect (- (:height pos-rect) (:height rect)))]
         y-diff (when pos-rect (- (:height pos-rect) (:height rect)))]
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
     [:div.absolute.rounded-md.shadow-lg.absolute-modal
      {:ref *el
      {:ref *el
-      :class (if x-overflow-vw? "is-overflow-vw-x" "")
+      :class (if y-overflow-vh? "is-overflow-vh-y" "")
       :on-mouse-down (fn [e]
       :on-mouse-down (fn [e]
                        (.stopPropagation e))
                        (.stopPropagation e))
       :style (merge
       :style (merge
@@ -525,10 +523,6 @@
         heading-class (get-editor-style-class content format)]
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
 
 
-     (when (= (state/sub :editor/record-status) "RECORDING")
-       [:div#audio-record-toolbar
-        (footer/audio-record-cp)])
-
      (ui/ls-textarea
      (ui/ls-textarea
       {:id                id
       {:id                id
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)

+ 6 - 3
src/main/frontend/components/editor.css

@@ -1,13 +1,12 @@
 #audio-record-toolbar {
 #audio-record-toolbar {
     position: fixed;
     position: fixed;
     background-color: var(--ls-secondary-background-color);
     background-color: var(--ls-secondary-background-color);
-    bottom: 45px;
-    width: 88px;
+    width: 90px;
     justify-content: left;
     justify-content: left;
     left: 5px;
     left: 5px;
     transition: none;
     transition: none;
     z-index: 9999;
     z-index: 9999;
-    padding: 5px 5px 0px 5px;
+    padding: 5px 5px 5px 8px;
     border-radius: 5px;
     border-radius: 5px;
 }
 }
 
 
@@ -36,6 +35,10 @@
   &.is-overflow-vw-x {
   &.is-overflow-vw-x {
     transform: translateX(calc(-100% + 1rem));
     transform: translateX(calc(-100% + 1rem));
   }
   }
+
+  &.is-overflow-vh-y {
+    transform: translateY(calc(-100% - 2rem));
+  }
 }
 }
 
 
 .is-mobile {
 .is-mobile {

+ 2 - 1
src/main/frontend/components/export.cljs

@@ -126,5 +126,6 @@
      [:div.mt-4
      [:div.mt-4
       (ui/button (if @copied? "Copied to clipboard!" "Copy to clipboard")
       (ui/button (if @copied? "Copied to clipboard!" "Copy to clipboard")
         :on-click (fn []
         :on-click (fn []
-                    (util/copy-to-clipboard! content (= type :html))
+                    (util/copy-to-clipboard! content (when (= type :html)
+                                                       content))
                     (reset! copied? true)))]]))
                     (reset! copied? true)))]]))

+ 4 - 4
src/main/frontend/components/file.cljs

@@ -5,7 +5,6 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
-            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -74,7 +73,8 @@
         format (gp-util/get-format path)
         format (gp-util/get-format path)
         original-name (db/get-file-page path)
         original-name (db/get-file-page path)
         random-id (str (d/squuid))]
         random-id (str (d/squuid))]
-    [:div.file {:id (str "file-edit-wrapper-" random-id)}
+    [:div.file {:id (str "file-edit-wrapper-" random-id)
+                :key path}
      [:h1.title
      [:h1.title
       [:bdi (js/decodeURI path)]]
       [:bdi (js/decodeURI path)]]
      (when original-name
      (when original-name
@@ -99,10 +99,10 @@
 
 
      (cond
      (cond
        ;; image type
        ;; image type
-       (and format (contains? (config/img-formats) format))
+       (and format (contains? (gp-config/img-formats) format))
        [:img {:src path}]
        [:img {:src path}]
 
 
-       (and format (contains? (config/text-formats) format))
+       (and format (contains? (gp-config/text-formats) format))
        (when-let [file-content (db/get-file path)]
        (when-let [file-content (db/get-file path)]
          (let [content (string/trim file-content)
          (let [content (string/trim file-content)
                mode (util/get-file-ext path)]
                mode (util/get-file-ext path)]

+ 26 - 18
src/main/frontend/components/header.cljs

@@ -133,8 +133,9 @@
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [:a#left-menu.cp__header-left-menu.button
     [:a#left-menu.cp__header-left-menu.button
      {:on-click on-click
      {:on-click on-click
-      :style {:margin-left 12}}
-     (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
+      :style    {:margin-left 12}}
+     [:span.inner
+      (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]]))
 
 
 (rum/defc dropdown-menu < rum/reactive
 (rum/defc dropdown-menu < rum/reactive
   [{:keys [current-repo t]}]
   [{:keys [current-repo t]}]
@@ -228,7 +229,11 @@
                                (or (empty? repos)
                                (or (empty? repos)
                                    (nil? (state/sub :git/current-repo)))
                                    (nil? (state/sub :git/current-repo)))
                                (not (mobile-util/native-platform?))
                                (not (mobile-util/native-platform?))
-                               (not config/publishing?))]
+                               (not config/publishing?))
+        left-menu (left-menu-button {:on-click (fn []
+                                       (open-fn)
+                                       (state/set-left-sidebar-open!
+                                        (not (:ui/left-sidebar-open? @state/state))))})]
     [:div.cp__header#head
     [:div.cp__header#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
      {:class           (util/classnames [{:electron-mac   electron-mac?
                                           :native-ios     (mobile-util/native-ios?)
                                           :native-ios     (mobile-util/native-ios?)
@@ -240,19 +245,23 @@
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
       :style           {:fontSize  50}}
       :style           {:fontSize  50}}
      [:div.l.flex
      [:div.l.flex
-      (left-menu-button {:on-click (fn []
-                                     (open-fn)
-                                     (state/set-left-sidebar-open!
-                                      (not (:ui/left-sidebar-open? @state/state))))})
-
-      (when current-repo ;; this is for the Search button
-        (ui/with-shortcut :go/search "right"
-          [:a.button#search-button
-           {:on-click #(do (when (or (mobile-util/native-android?)
-                                     (mobile-util/native-iphone?))
-                             (state/set-left-sidebar-open! false))
-                           (state/pub-event! [:go/search]))}
-           (ui/icon "search" {:style {:fontSize ui/icon-size}})]))]
+      (when-not (mobile-util/native-platform?)
+        [left-menu
+         (when current-repo ;; this is for the Search button
+           (ui/with-shortcut :go/search "right"
+             [:a.button#search-button
+              {:on-click #(do (when (or (mobile-util/native-android?)
+                                        (mobile-util/native-iphone?))
+                                (state/set-left-sidebar-open! false))
+                              (state/pub-event! [:go/search]))}
+              (ui/icon "search" {:style {:fontSize ui/icon-size}})]))])
+      (when (mobile-util/native-platform?)
+        (if (state/home?)
+          left-menu
+          (ui/with-shortcut :go/backward "bottom"
+            [:a.it.navigation.nav-left.button
+             {:title "Go back" :on-click #(js/window.history.back)}
+             (ui/icon "chevron-left" {:style {:fontSize 25}})])))]
 
 
      [:div.r.flex
      [:div.r.flex
       (when-not file-sync-handler/hiding-login&file-sync
       (when-not file-sync-handler/hiding-login&file-sync
@@ -265,8 +274,7 @@
       (when (not= (state/get-current-route) :home)
       (when (not= (state/get-current-route) :home)
         (home-button))
         (home-button))
 
 
-      (when (or (util/electron?)
-                (mobile-util/native-ios?))
+      (when (util/electron?)
         (back-and-forward))
         (back-and-forward))
 
 
       (when-not (mobile-util/native-platform?)
       (when-not (mobile-util/native-platform?)

+ 108 - 101
src/main/frontend/components/header.css

@@ -1,5 +1,5 @@
 .cp__header {
 .cp__header {
-  @apply shadow z-10;
+  @apply z-10;
   -webkit-app-region: drag;
   -webkit-app-region: drag;
 
 
   padding-top: var(--ls-headbar-inner-top-padding);
   padding-top: var(--ls-headbar-inner-top-padding);
@@ -31,9 +31,10 @@
   }
   }
 
 
   /* To prevent header glitch on Safari */
   /* To prevent header glitch on Safari */
+
   > .l, > .r {
   > .l, > .r {
-      -webkit-transform: translate3d(0, 0, 0);
-      transform: translate3d(0, 0, 0);
+    -webkit-transform: translate3d(0, 0, 0);
+    transform: translate3d(0, 0, 0);
   }
   }
 
 
   .it svg {
   .it svg {
@@ -104,7 +105,6 @@
 
 
   .dropdown-wrapper {
   .dropdown-wrapper {
     .ti {
     .ti {
-      margin-right: 5px;
       opacity: .9;
       opacity: .9;
     }
     }
   }
   }
@@ -119,6 +119,18 @@
       top: 1px;
       top: 1px;
     }
     }
   }
   }
+
+  &-left-menu {
+    &.button {
+      margin: 0;
+      padding: 0;
+    }
+
+    > .inner {
+      line-height: 0;
+      padding: 3px;
+    }
+  }
 }
 }
 
 
 .is-electron.is-mac .cp__header {
 .is-electron.is-mac .cp__header {
@@ -191,7 +203,7 @@ a.button {
     background: none;
     background: none;
 
 
     @screen md {
     @screen md {
-        background: var(--ls-tertiary-background-color);
+      background: var(--ls-tertiary-background-color);
     }
     }
   }
   }
 
 
@@ -205,6 +217,7 @@ a.button {
 }
 }
 
 
 html.is-ios.is-safari {
 html.is-ios.is-safari {
+
   .cp__header {
   .cp__header {
     background-color: var(--ls-primary-background-color);
     background-color: var(--ls-primary-background-color);
   }
   }
@@ -218,136 +231,130 @@ html.is-native-iphone,
 html.is-native-iphone-without-notch,
 html.is-native-iphone-without-notch,
 html.is-native-ipad {
 html.is-native-ipad {
 
 
-     #main-container {
-         padding-top: 0px;
-         display: flex;
-         flex-direction: column;
-    }
+  #main-container {
+    padding-top: 0px;
+    display: flex;
+    flex-direction: column;
+  }
 
 
-     #main-content-container {
-         padding-left: 22px;
-         padding-right: 14px;
-         padding-top: 0px;
-         height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
+  #main-content-container {
+    padding-left: 22px;
+    padding-right: 14px;
+    padding-top: 0px;
+    height: calc(100vh - var(--ls-headbar-inner-top-padding) - var(--ls-headbar-height));
 
 
-         @screen sm {
-             padding-left: 2rem;
-         }
+    @screen sm {
+      padding-left: 2rem;
+    }
 
 
-         .page {
-             margin-top: 24px;
-         }
+    .page {
+      margin-top: 24px;
     }
     }
+  }
 
 
-    .cp__header > .r {
-        display: flex;
+  .cp__header {
+    > .r {
+      display: flex;
+    }
+
+    a.button {
+      opacity: 1;
     }
     }
+  }
 }
 }
 
 
 html.is-native-ipad {
 html.is-native-ipad {
-    --ls-headbar-inner-top-padding: 0px;
-    --ls-headbar-height: 4rem;
+  --ls-headbar-inner-top-padding: 0px;
+  --ls-headbar-height: 4rem;
 
 
-    .cp__header {
-      background-color: transparent !important;
-      display: flex;
+  .cp__header {
+    background-color: transparent !important;
+    display: flex;
 
 
-      > .l {
-        /* background-color: var(--ls-primary-background-color); */
-        padding-top: 20px;
-      }
+    > .l {
+      /* background-color: var(--ls-primary-background-color); */
+      padding-top: 20px;
+    }
 
 
-      > .r {
-        flex: 1;
-        background-color: var(--ls-primary-background-color);
-        height: 100%;
-        padding-top: 20px;
-        justify-content: flex-end;
-        align-items: center;
-      }
+    > .r {
+      flex: 1;
+      background-color: var(--ls-primary-background-color);
+      height: 100%;
+      padding-top: 20px;
+      justify-content: flex-end;
+      align-items: center;
     }
     }
+  }
 
 
-    .left-sidebar-inner  {
-        > .wrap {
-            padding-top: 20px;
-      }
+  .left-sidebar-inner  {
+    > .wrap {
+      padding-top: 20px;
+    }
+  }
+
+  .cp__right-sidebar {
+    &-settings {
+      margin-top: -4px;
     }
     }
 
 
-    .cp__right-sidebar {
-        &-settings {
-            margin-top: -4px;
-        }
-
-        &-topbar {
-            padding-top: 37px;
-        }
-
-        &-inner {
-            .resizer {
-                top: 30vh;
-                width: 12px;
-                height: 40vh;
-            }
-
-            .resizer:hover {
-                background-color: var(--ls-guideline-color, #ddd);
-            }
-        }
+    &-topbar {
+      padding-top: 37px;
     }
     }
+
+    &-inner {
+      .resizer {
+        top: 30vh;
+        width: 12px;
+        height: 40vh;
+      }
+
+      .resizer:hover {
+        background-color: var(--ls-guideline-color, #ddd);
+      }
+    }
+  }
 }
 }
 
 
 html.is-native-iphone {
 html.is-native-iphone {
-    --ls-headbar-inner-top-padding: 36px;
+  --ls-headbar-inner-top-padding: 36px;
 
 
-    .left-sidebar-inner {
-        > .wrap {
-            padding-top: 12px;
-        }
-
-        .new-page {
-            padding-bottom: 12px;
-        }
+  .left-sidebar-inner {
+    > .wrap {
+      padding-top: 12px;
     }
     }
 
 
-    .ui__notifications {
-        top: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding) - 0.3rem);
+    .new-page {
+      padding-bottom: 12px;
     }
     }
+  }
 
 
-    @media (orientation: landscape) {
-        --ls-headbar-inner-top-padding: 8px;
-        --ls-headbar-height: 2.5rem;
+  .ui__notifications {
+    top: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding) - 0.3rem);
+  }
 
 
-        .cp__header {
-            @apply shadow z-10;
-        }
-    }
+  @media (orientation: landscape) {
+    --ls-headbar-inner-top-padding: 8px;
+    --ls-headbar-height: 2.5rem;
+  }
 }
 }
 
 
 html.is-native-iphone-without-notch {
 html.is-native-iphone-without-notch {
 
 
-    --ls-headbar-inner-top-padding: 15px;
-    --ls-headbar-height: 2.5rem;
+  --ls-headbar-inner-top-padding: 15px;
+  --ls-headbar-height: 2.5rem;
 
 
-    @media (orientation: landscape) {
+  @media (orientation: landscape) {
 
 
-        --ls-headbar-inner-top-padding: 0px;
-        --ls-headbar-height: 2.5rem;
-
-        .cp__header {
-            @apply shadow z-10;
-        }
-    }
+    --ls-headbar-inner-top-padding: 0px;
+    --ls-headbar-height: 2.5rem;
+  }
 }
 }
 
 
 html.is-zoomed-native-ios {
 html.is-zoomed-native-ios {
-    --ls-headbar-inner-top-padding: 30px;
-
-     @media (orientation: landscape) {
-        --ls-headbar-inner-top-padding: 8px;
-        --ls-headbar-height: 2.5rem;
+  --ls-headbar-inner-top-padding: 30px;
 
 
-        .cp__header {
-            @apply shadow z-10;
-        }
-    }
+  @media (orientation: landscape) {
+    --ls-headbar-inner-top-padding: 8px;
+    --ls-headbar-height: 2.5rem;
+  }
 }
 }

+ 2 - 2
src/main/frontend/components/journal.cljs

@@ -8,10 +8,10 @@
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.state :as state]
             [frontend.state :as state]
-            [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
+            [frontend.util.text :as text-util]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
             [rum.core :as rum]))
@@ -32,7 +32,7 @@
         page-entity (db/pull [:block/name (util/page-name-sanity-lc title)])
         page-entity (db/pull [:block/name (util/page-name-sanity-lc title)])
         data-page-tags (when (seq (:block/tags page-entity))
         data-page-tags (when (seq (:block/tags page-entity))
                          (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
                          (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
-                           (text/build-data-value page-names)))]
+                           (text-util/build-data-value page-names)))]
     [:div.flex-1.journal.page (cond-> {}
     [:div.flex-1.journal.page (cond-> {}
                                 data-page-tags
                                 data-page-tags
                                 (assoc :data-page-tags data-page-tags))
                                 (assoc :data-page-tags data-page-tags))

+ 557 - 334
src/main/frontend/components/onboarding/index.css

@@ -1,393 +1,616 @@
 body[data-page=repo-add],
 body[data-page=repo-add],
 body[data-page=import] {
 body[data-page=import] {
-    .cp__header .add-graph-btn {
-        display: none;
-    }
+  .cp__header .add-graph-btn {
+    display: none;
+  }
 }
 }
 
 
 .cp__onboarding {
 .cp__onboarding {
-    &-setups {
-        background-color: var(--ls-primary-background-color);
-        z-index: 1;
+  &-setups {
+    background-color: var(--ls-primary-background-color);
+    z-index: 1;
+
+    .as-flex-center {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .inner-card {
+      background-color: var(--ls-secondary-background-color);
+      padding-top: 55px;
 
 
-        .as-flex-center {
+      > h1 {
+        font-size: 30px;
+
+        strong {
+          color: var(--ls-active-primary-color);
+          display: inline-block;
+          padding-right: 4px;
+          font-weight: normal;
+
+          &:first-child {
+            display: block;
+            text-align: center;
+            padding-bottom: 15px;
+          }
+        }
+      }
+
+      > h2 {
+        font-size: 14px;
+        margin-top: 20px;
+        margin-bottom: 40px;
+        padding: 0 30px;
+        text-align: center;
+      }
+
+      > article {
+        background-color: var(--ls-secondary-background-color);
+        border-radius: 20px;
+        flex: 1;
+
+        > section {
+          flex: 1;
+
+          &.a {
+            background-color: var(--ls-tertiary-background-color);
             display: flex;
             display: flex;
+            flex-direction: column;
             align-items: center;
             align-items: center;
-            justify-content: center;
-        }
+            padding-top: 75px;
+            padding-bottom: 100px;
+            border-radius: 20px 20px 0 0;
+            min-width: 50%;
+
+            > strong {
+              font-size: 28px;
+              padding-bottom: 15px;
+              font-weight: 600;
+            }
 
 
-        .inner-card {
-            background-color: var(--ls-secondary-background-color);
-            padding-top: 55px;
+            > small {
+              font-size: 18px;
+              line-height: 30px;
+              padding: 0 40px;
+              text-align: center;
+            }
 
 
-            > h1 {
-                font-size: 30px;
+            > .choose {
+              padding-top: 48px;
+              width: 100%;
+              cursor: pointer;
 
 
-                strong {
-                    color: var(--ls-active-primary-color);
-                    display: inline-block;
-                    padding-right: 4px;
-                    font-weight: normal;
-
-                    &:first-child {
-                        display: block;
-                        text-align: center;
-                        padding-bottom: 15px;
-                    }
-                }
-            }
+              &:active {
+                opacity: .8;
+              }
 
 
-            > h2 {
-                font-size: 14px;
-                margin-top: 20px;
-                margin-bottom: 40px;
-                padding: 0 30px;
-                text-align: center;
+              i {
+                width: 180px;
+                height: 120px;
+                background-image: url("../img/folder-logo.png");
+                background-size: contain;
+              }
+
+              .control {
+                padding: 20px 20px 0 20px;
+                width: 100%;
+              }
             }
             }
+          }
 
 
-            > article {
-                background-color: var(--ls-secondary-background-color);
-                border-radius: 20px;
-                flex: 1;
+          &.b {
+            display: none;
+            padding: 50px 40px;
 
 
-                > section {
-                    flex: 1;
-
-                    &.a {
-                        background-color: var(--ls-tertiary-background-color);
-                        display: flex;
-                        flex-direction: column;
-                        align-items: center;
-                        padding-top: 75px;
-                        padding-bottom: 100px;
-                        border-radius: 20px 20px 0 0;
-                        min-width: 50%;
-
-                        > strong {
-                            font-size: 28px;
-                            padding-bottom: 15px;
-                            font-weight: 600;
-                        }
-
-                        > small {
-                            font-size: 18px;
-                            line-height: 30px;
-                            padding: 0 40px;
-                            text-align: center;
-                        }
-
-                        > .choose {
-                            padding-top: 48px;
-                            width: 100%;
-                            cursor: pointer;
-
-                            &:active {
-                                opacity: .8;
-                            }
-
-                            i {
-                                width: 180px;
-                                height: 120px;
-                                background-image: url("../img/folder-logo.png");
-                                background-size: contain;
-                            }
-
-                            .control {
-                                padding: 20px 20px 0 20px;
-                                width: 100%;
-                            }
-                        }
-                    }
+            > p {
+              width: 100%;
 
 
-                    &.b {
-                        display: none;
-                        padding: 50px 40px;
-
-                        > p {
-                            width: 100%;
-
-                            &:first-child {
-                                i {
-                                    width: 42px;
-                                }
-
-                                strong {
-                                    font-size: 19px;
-                                    line-height: 1.5em;
-                                }
-                            }
-                        }
-
-                        ul {
-                            margin: 0;
-                            padding-top: 20px;
-                            width: 100%;
-
-                            > li {
-                                list-style: none;
-                                margin-bottom: 8px;
-                                height: 40px;
-                                display: flex;
-
-                                &.hr {
-                                    height: 1px;
-                                    border-bottom: 1px solid var(--ls-border-color);
-                                    margin: 20px 0px;
-                                    opacity: 0.6;
-                                }
-
-                                > i {
-                                    width: 48px;
-                                    background-image: url(../img/folder.png);
-                                    background-size: contain;
-                                    background-repeat: no-repeat;
-                                    background-position-y: 2px;
-                                    position: relative;
-                                    right: -2px;
-
-                                    &.is-file {
-                                        background-image: url("../img/file-edn.png");
-                                        background-size: 50%;
-                                        background-position-x: center;
-                                    }
-
-                                    > .ti {
-                                        opacity: .25;
-                                        color: black;
-                                    }
-                                }
-
-                                > span {
-                                    flex: 1;
-                                    padding-left: 15px;
-                                    display: flex;
-                                    flex-direction: column;
-
-                                    strong {
-                                        font-size: 11px;
-                                    }
-                                }
-                            }
-                        }
-                    }
+              &:first-child {
+                i {
+                  width: 42px;
                 }
                 }
 
 
-                &.importer {
-                    background-color: var(--ls-tertiary-background-color);
-                    position: relative;
-
-                    > section {
-                        flex: unset;
-                        padding: 0 30px;
-
-                        h1 {
-                            font-size: 26px;
-                            font-weight: 500;
-                        }
-
-                        h2 {
-                            font-size: 14px;
-                            padding-top: 15px;
-                            padding-bottom: 15px;
-                        }
-
-                        &.d {
-                            width: 100%;
-                            padding: 20px 0;
-
-                            > label {
-                                width: unset;
-                                height: 80px;
-                                flex: 1;
-                                margin-bottom: 10px;
-
-                                > span {
-                                    &:first-child {
-                                        padding: 16px;
-
-                                        i {
-                                            height: 30px;
-                                            width: 30px;
-                                            display: flex;
-                                            align-items: center;
-                                            justify-content: center;
-                                        }
-                                    }
-                                }
-
-                                strong {
-                                    font-size: 18px;
-                                }
-
-                                small {
-                                    font-size: 11px;
-                                }
-                            }
-                        }
-                    }
+                strong {
+                  font-size: 19px;
+                  line-height: 1.5em;
                 }
                 }
+              }
             }
             }
 
 
+            ul {
+              margin: 0;
+              padding-top: 20px;
+              width: 100%;
+
+              > li {
+                list-style: none;
+                margin-bottom: 8px;
+                height: 40px;
+                display: flex;
+
+                &.hr {
+                  height: 1px;
+                  border-bottom: 1px solid var(--ls-border-color);
+                  margin: 20px 0px;
+                  opacity: 0.6;
+                }
 
 
-            @screen lg {
-                background-color: unset;
+                > i {
+                  width: 48px;
+                  background-image: url(../img/folder.png);
+                  background-size: contain;
+                  background-repeat: no-repeat;
+                  background-position-y: 2px;
+                  position: relative;
+                  right: -2px;
+
+                  &.is-file {
+                    background-image: url("../img/file-edn.png");
+                    background-size: 50%;
+                    background-position-x: center;
+                  }
+
+                  > .ti {
+                    opacity: .25;
+                    color: black;
+                  }
+                }
 
 
-                > h1 {
-                    font-size: 36px;
+                > span {
+                  flex: 1;
+                  padding-left: 15px;
+                  display: flex;
+                  flex-direction: column;
 
 
-                    strong {
-                        &:first-child {
-                            display: inline-block;
-                        }
-                    }
+                  strong {
+                    font-size: 11px;
+                  }
                 }
                 }
+              }
+            }
+          }
+        }
 
 
-                > h2 {
-                    font-size: 20px;
-                    padding: 0;
-                }
+        &.importer {
+          background-color: var(--ls-tertiary-background-color);
+          position: relative;
 
 
-                > article {
-                    max-width: 100vw;
-                    flex: unset;
+          > section {
+            flex: unset;
+            padding: 0 30px;
 
 
-                    > section {
-                        &.a {
-                            border-radius: 20px 0 0 20px;
+            h1 {
+              font-size: 26px;
+              font-weight: 500;
+            }
 
 
-                            > small {
-                                padding: 0 95px;
-                            }
+            h2 {
+              font-size: 14px;
+              padding-top: 15px;
+              padding-bottom: 15px;
+            }
 
 
-                            > .choose {
-                                .control {
-                                    padding: 30px 70px 0 70px;
-                                }
-                            }
-                        }
+            &.d {
+              width: 100%;
+              padding: 20px 0;
 
 
-                        &.b {
-                            display: flex;
-                        }
+              > label {
+                width: unset;
+                height: 80px;
+                flex: 1;
+                margin-bottom: 10px;
+
+                > span {
+                  &:first-child {
+                    padding: 16px;
+
+                    i {
+                      height: 30px;
+                      width: 30px;
+                      display: flex;
+                      align-items: center;
+                      justify-content: center;
                     }
                     }
+                  }
+                }
 
 
-                    &.importer {
-                        > section {
-                            padding: 0;
-
-                            h1 {
-                                font-size: 28px;
-                            }
-
-                            h2 {
-                                font-size: 19px;
-                                line-height: 3em;
-                                padding: unset;
-                            }
-
-                            &.e {
-                                position: absolute;
-                                bottom: -50px;
-                                right: 15px;
-
-                                a.button {
-                                    padding: 4px 10px;
-                                }
-                            }
-                        }
-                    }
+                strong {
+                  font-size: 18px;
+                }
+
+                small {
+                  font-size: 11px;
                 }
                 }
+              }
             }
             }
+          }
         }
         }
+      }
 
 
-        label.action-input {
-            transition: none;
-            color: var(--ls-active-primary-color);
-            background-color: var(--ls-quaternary-background-color);
-            height: 68px;
-            width: 100%;
-            opacity: .8;
-            user-select: none;
-            border-radius: 12px;
-            overflow: hidden;
-            cursor: pointer;
-
-            small {
-                font-size: 11px;
-                text-align: center;
+
+      @screen lg {
+        background-color: unset;
+
+        > h1 {
+          font-size: 36px;
+
+          strong {
+            &:first-child {
+              display: inline-block;
             }
             }
+          }
+        }
 
 
-            &:hover {
-                opacity: 1;
+        > h2 {
+          font-size: 20px;
+          padding: 0;
+        }
+
+        > article {
+          max-width: 100vw;
+          flex: unset;
+
+          > section {
+            &.a {
+              border-radius: 20px 0 0 20px;
+
+              > small {
+                padding: 0 95px;
+              }
+
+              > .choose {
+                .control {
+                  padding: 30px 70px 0 70px;
+                }
+              }
             }
             }
 
 
-            &:active {
-                opacity: .5;
+            &.b {
+              display: flex;
             }
             }
+          }
+
+          &.importer {
+            > section {
+              padding: 0;
 
 
-            &[disabled] {
-                pointer-events: none;
+              h1 {
+                font-size: 28px;
+              }
+
+              h2 {
+                font-size: 19px;
+                line-height: 3em;
+                padding: unset;
+              }
+
+              &.e {
+                position: absolute;
+                bottom: -50px;
+                right: 15px;
+
+                a.button {
+                  padding: 4px 10px;
+                }
+              }
             }
             }
+          }
         }
         }
+      }
+    }
 
 
-        @screen lg {
-            justify-content: center;
-            align-items: center;
+    label.action-input {
+      transition: none;
+      color: var(--ls-active-primary-color);
+      background-color: var(--ls-quaternary-background-color);
+      height: 68px;
+      width: 100%;
+      opacity: .8;
+      user-select: none;
+      border-radius: 12px;
+      overflow: hidden;
+      cursor: pointer;
+
+      small {
+        font-size: 11px;
+        text-align: center;
+      }
+
+      &:hover {
+        opacity: 1;
+      }
+
+      &:active {
+        opacity: .5;
+      }
+
+      &[disabled] {
+        pointer-events: none;
+      }
+    }
+
+    @screen lg {
+      justify-content: center;
+      align-items: center;
+    }
+  }
+
+  &-quick-tour {
+    &.shepherd-element, .shepherd-arrow:before {
+      background-color: var(--ls-quaternary-background-color);
+      font-size: 14px;
+    }
+
+    &.shepherd-element {
+      max-width: 320px;
+      border-radius: 8px;
+      box-shadow: none;
+
+      &[data-shepherd-step-id=nav-journal-page] {
+        .shepherd-arrow {
+          left: 30px !important;
         }
         }
+      }
     }
     }
+
+    .shepherd-content {
+      display: flex;
+      flex-direction: column;
+      -webkit-font-smoothing: antialiased;
+    }
+
+    .shepherd-text {
+      font-size: 14px;
+      color: var(--ls-secondary-text-color);
+      flex: 1;
+      padding: 8px 10px;
+      position: relative;
+
+      h2 {
+        font-size: 18px;
+        line-height: 28px;
+        font-weight: 700;
+        padding-top: 5px;
+        padding-bottom: 5px;
+      }
+
+      p {
+        color: var(--ls-secondary-text-color);
+      }
+
+      .steps {
+        position: absolute;
+        bottom: -28px;
+        left: 10px;
+        font-size: 12px;
+
+        strong {
+          opacity: .5;
+          font-size: 11px;
+        }
+
+        ul {
+          display: flex;
+          list-style: none;
+          align-items: center;
+          margin: 0;
+          padding-left: 1px;
+          padding-top: 2px;
+
+          li {
+            padding: 0;
+            width: 5px;
+            height: 5px;
+            margin: 0 5px 0 0;
+            background-color: rgba(0, 0, 0, .2);
+            border-radius: 100%;
+            font-size: 0;
+
+            &.active {
+              background-color: var(--ls-primary-text-color);
+            }
+          }
+        }
+      }
+    }
+
+    .shepherd-footer {
+      button {
+        @apply bg-indigo-600 hover:bg-indigo-700;
+
+        padding: 4px 8px;
+        border-radius: 6px;
+        overflow: hidden;
+        color: white;
+        font-size: 13px;
+        font-weight: 500;
+
+        &.back {
+          background-color: rgba(0, 0, 0, .3);
+        }
+      }
+    }
+  }
+
+  &-skip-quick-tour {
+    -webkit-font-smoothing: antialiased;
+    position: fixed;
+    bottom: 20px;
+    left: 50%;
+    transform: translateX(-60px);
+    z-index: 10001;
+    background-color: #4B5563;
+    padding: 12px 20px;
+    border-radius: 8px;
+    color: white;
+    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
+    font-weight: 700;
+
+    .ti {
+      padding-right: 6px;
+      font-size: 18px;
+      position: relative;
+      top: 2px;
+      font-weight: normal;
+    }
+
+    &:hover {
+      opacity: .8;
+    }
+
+    &:active {
+      opacity: 1;
+    }
+  }
+}
+
+.shepherd-modal-overlay-container.shepherd-modal-is-visible {
+  transition: none;
+  opacity: .4;
+}
+
+.shepherd-target {
+  &.nav-content-item {
+    background-color: rgba(0, 0, 0, .4);
+    padding-left: 15px;
+    padding-right: 15px;
+  }
+
+  .nav-content-item-inner {
+    background-color: var(--ls-secondary-background-color);
+    overflow: hidden;
+
+    .header {
+      padding: 4px 10px !important;
+    }
+
+    .bd {
+      ul a {
+        padding: 2px 4px !important;
+      }
+    }
+  }
+
+  &.page-title {
+    background-color: rgba(0, 0, 0, .4);
+
+    > h1 {
+      background-color: var(--ls-primary-background-color);
+      border-radius: 12px;
+    }
+  }
+
+  &.cp__sidebar-help-btn {
+    background-color: rgba(0, 0, 0, .4);
+
+    > .inner {
+      opacity: 1;
+    }
+  }
+
+  &.cp__header-left-menu {
+    background-color: rgba(0, 0, 0, .4);
+    border-radius: 0;
+    opacity: 1 !important;
+    transition: none;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, .4);
+    }
+
+    > .inner {
+      border-radius: 100%;
+      background-color: var(--ls-primary-background-color);
+    }
+  }
+}
+
+.shepherd-element {
+  &[data-popper-placement^=right] > .shepherd-arrow {
+    left: -4px;
+  }
+
+  &[data-popper-placement^=top] > .shepherd-arrow {
+    bottom: -4px !important;
+  }
+
+  &[data-popper-placement^=left] > .shepherd-arrow {
+    right: -4px !important;
+  }
+
+  &[data-popper-placement^=bottom] > .shepherd-arrow {
+    top: -4px !important;
+  }
+}
+
+.shepherd-arrow, .shepherd-arrow:before {
+  width: 8px;
+  height: 8px;
+}
+
+html[data-theme=light] {
+  .cp__onboarding {
+    &-quick-tour {
+      &.shepherd-element, .shepherd-arrow:before {
+        background-color: #E8E5DE;
+      }
+    }
+  }
 }
 }
 
 
 html.is-native-android,
 html.is-native-android,
 html.is-native-ipad,
 html.is-native-ipad,
 html.is-native-iphone,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
 html.is-native-iphone-without-notch {
-    .cp__onboarding {
-        &-setups {
-            position: absolute;
-            width: 100%;
-            top: 0;
-            left: 0;
-            height: 100vh;
-            overflow-y: auto;
-
-            .inner-card {
-                padding-top: 30px;
-                min-height: 100vh;
-                width: 100%;
-
-                > h2 {
-                    margin-bottom: 25px;
-                }
-
-                > article {
-                    > section {
-                        &.a {
-                            padding-top: 25px;
-
-                            small {
-                                text-align: left;
-                                padding: 0 40px;
-                            }
-                            .mobile-intro {
-                                margin-top: 10px;
-                                line-height: 1.5rem;
-                                font-size: 1rem;
-                            }
-
-                            > .choose {
-                                padding-top: 25px;
-                            }
-                        }
-                    }
-                }
+  .cp__onboarding {
+    &-setups {
+      position: absolute;
+      width: 100%;
+      top: 0;
+      left: 0;
+      height: 100vh;
+      overflow-y: auto;
+
+      .inner-card {
+        padding-top: 30px;
+        min-height: 100vh;
+        width: 100%;
+
+        > h2 {
+          margin-bottom: 25px;
+        }
 
 
-                @screen lg {
-                    > article {
-                        width: 1040px;
-                        height: 510px;
-                    }
-                }
+        > article {
+          > section {
+            &.a {
+              padding-top: 25px;
+
+              small {
+                text-align: left;
+                padding: 0 40px;
+              }
+
+              .mobile-intro {
+                margin-top: 10px;
+                line-height: 1.5rem;
+                font-size: 1rem;
+              }
+
+              > .choose {
+                padding-top: 25px;
+              }
             }
             }
+          }
+        }
+
+        @screen lg {
+          > article {
+            width: 1040px;
+            height: 510px;
+          }
         }
         }
+      }
     }
     }
+  }
 }
 }

+ 158 - 0
src/main/frontend/components/onboarding/quick_tour.cljs

@@ -0,0 +1,158 @@
+(ns frontend.components.onboarding.quick-tour
+  (:require [promesa.core :as p]
+            [cljs-bean.core :as bean]
+            [frontend.loader :refer [load]]
+            [frontend.state :as state]
+            [frontend.date :as date]
+            [frontend.util :as util]
+            [frontend.handler.route :as router-handler]
+            [frontend.handler.command-palette :as command-palette]
+            [hiccups.runtime :as h]
+            [dommy.core :as d]))
+
+(defn js-load$
+  [url]
+  (p/create
+    (fn [resolve]
+      (load url resolve))))
+
+(def JS_ROOT
+  (if (= js/location.protocol "file:")
+    "./js"
+    "./static/js"))
+
+(defn- load-base-assets$
+  []
+  (js-load$ (str JS_ROOT "/shepherd.min.js")))
+
+(defn- make-skip-fns
+  [^js jsTour]
+  (let [^js el (js/document.createElement "button")]
+    (.add (.-classList el) "cp__onboarding-skip-quick-tour")
+    (set! (.-innerHTML el) (h/render-html [:span [:i.ti.ti-player-skip-forward] "Skip Quick Tour"]))
+    (.addEventListener el "click" #(.cancel jsTour))
+    [#(.appendChild js/document.body el)
+     #(.removeChild js/document.body el)]))
+
+(defn- wait-target
+  [fn-or-selector time]
+  (p/let [action (if (string? fn-or-selector)
+                   #(d/sel1 fn-or-selector)
+                   fn-or-selector)
+          _      (action)
+          _      (p/delay time)]))
+
+(defn- inject-steps-indicator
+  [current total]
+
+  (h/render-html
+    [:div.steps
+     [:strong (str "STEP " current)]
+     [:ul (for [i (range total)] [:li {:class (when (= current (inc i)) "active")} i])]]))
+
+(defn- create-steps! [^js jsTour]
+  [
+   ;; step 1
+   {:id                "nav-help"
+    :text              (h/render-html [:section [:h2 "❓ Help"]
+                                       [:p "You can always click here for help and other information about Logseq."]])
+    :attachTo          {:element ".cp__sidebar-help-btn" :on "top"}
+    :beforeShowPromise #(if (state/sub :ui/sidebar-open?)
+                          (wait-target state/hide-right-sidebar! 700)
+                          (p/resolved true))
+    :canClickTarget    true
+    :buttons           [{:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 20}}
+                                    {:name    "offset"
+                                     :options {:offset [0, 10]}}]}}
+
+   ;; step 2
+   {:id                "nav-journal-page"
+    :text              (h/render-html [:section [:h2 "📆 Daily Journal Page"]
+                                       [:p
+                                        [:span "This is today’s daily journal page. Here yo can dump your thoughts, learnings and ideas. Don’t worry about organizing. Just write and"]
+                                        [:a "[[link]]"]
+                                        [:span "your thoughts."]]])
+
+    :attachTo          {:element ".page.is-journals .page-title" :on "top-end"}
+    :beforeShowPromise #(if-not (= (util/safe-lower-case (state/get-current-page))
+                                  (util/safe-lower-case (date/today)))
+                          (wait-target (fn []
+                                         (router-handler/redirect-to-page! (date/today))
+                                         (util/scroll-to-top)) 200)
+                          (p/resolved true))
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 63}}
+                                    {:name    "offset"
+                                     :options {:offset [10, 10]}}]}}
+
+   ;; step 3
+   {:id                "nav-left-sidebar"
+    :text              (h/render-html [:section [:h2 "👀 Left Sidebar"]
+                                       [:p [:span "Open the left sidebar to explore important menu items in Logseq."]]])
+
+    :attachTo          {:element "#left-menu" :on "top"}
+    :beforeShowPromise #(p/resolved true)
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Next" :action (.-next jsTour)}]
+    :popperOptions     {:modifiers [{:name    "preventOverflow"
+                                     :options {:padding 20}}
+                                    {:name    "offset"
+                                     :options {:offset [10, 10]}}]}}
+
+   ;; step 4
+   {:id                "nav-favorites"
+    :text              (h/render-html [:section [:h2 "⭐️ Favorites"]
+                                       [:p "Pin your favorite pages via the `... `menu on any page."]
+                                       [:p "We’ve also added some template pages here to help you get started. You can remove these once you start writing your own notes."]])
+    :beforeShowPromise #(if-not (state/sub :ui/left-sidebar-open?)
+                          (wait-target state/toggle-left-sidebar! 500)
+                          (p/resolved true))
+    :attachTo          {:element ".nav-content-item.favorites" :on "right"}
+    :buttons           [{:text "Back" :classes "back" :action (.-back jsTour)}
+                        {:text "Finish" :action (.-complete jsTour)}]}
+   ])
+
+(defn start
+  []
+  (let [^js jsTour (js/Shepherd.Tour.
+                     (bean/->js
+                       {:useModalOverlay    true
+                        :defaultStepOptions {:classes  "cp__onboarding-quick-tour"
+                                             :scrollTo false}}))
+        steps      (create-steps! jsTour)
+        steps      (map-indexed #(assoc %2 :text (str (:text %2) (inject-steps-indicator (inc %1) (count steps)))) steps)
+        [show-skip! hide-skip!] (make-skip-fns jsTour)]
+
+    ;; events
+    (doto jsTour
+      (.on "show" show-skip!)
+      (.on "hide" hide-skip!)
+      (.on "complete" hide-skip!)
+      (.on "cancel" hide-skip!))
+
+    (doseq [step steps]
+      (.addStep jsTour (bean/->js step)))
+
+    (.start jsTour)))
+
+(defn- ready
+  [callback]
+  (p/then
+    (if (nil? js/window.Shepherd)
+      (load-base-assets$) (p/resolved true))
+    callback))
+
+(def should-guide? false)
+
+(defn init []
+  (command-palette/register {:id     :document/quick-tour
+                             :desc   "Quick tour for onboarding"
+                             :action #(ready start)})
+
+  ;; TODO: fix logic
+  (when should-guide?
+    (ready start)))

+ 155 - 150
src/main/frontend/components/page.cljs

@@ -25,11 +25,12 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
-            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
             [frontend.search :as search]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
+            [frontend.util.text :as text-util]
             [goog.object :as gobj]
             [goog.object :as gobj]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
@@ -353,7 +354,7 @@
       [:div.flex-1.page.relative
       [:div.flex-1.page.relative
        (merge (if (seq (:block/tags page))
        (merge (if (seq (:block/tags page))
                 (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
                 (let [page-names (model/get-page-names-by-ids (map :db/id (:block/tags page)))]
-                  {:data-page-tags (text/build-data-value page-names)})
+                  {:data-page-tags (text-util/build-data-value page-names)})
                 {})
                 {})
 
 
               {:key path-page-name
               {:key path-page-name
@@ -680,7 +681,7 @@
   [title key by-item desc?]
   [title key by-item desc?]
   [:th
   [:th
    {:class [(name key)]}
    {:class [(name key)]}
-   [:a {:on-click (fn []
+   [:a.fade-link {:on-click (fn []
                     (reset! by-item key)
                     (reset! by-item key)
                     (swap! desc? not))}
                     (swap! desc? not))}
     [:span.flex.items-center
     [:span.flex.items-center
@@ -750,10 +751,6 @@
   (rum/local false ::journals)
   (rum/local false ::journals)
   (rum/local nil ::filter-fn)
   (rum/local nil ::filter-fn)
   (rum/local 1 ::current-page)
   (rum/local 1 ::current-page)
-  ;; {:did-mount (fn [state]
-  ;;               (let [current-repo (state/sub :git/current-repo)]
-  ;;                 (js/setTimeout #(db/remove-orphaned-pages! current-repo) 0))
-  ;;               state)}
   [state]
   [state]
   (let [current-repo (state/sub :git/current-repo)
   (let [current-repo (state/sub :git/current-repo)
         per-page-num 40
         per-page-num 40
@@ -801,6 +798,8 @@
     [:div.flex-1.cp__all_pages
     [:div.flex-1.cp__all_pages
      [:h1.title (t :all-pages)]
      [:h1.title (t :all-pages)]
 
 
+     [:div.text-sm.ml-1.opacity-70.mb-4 (t :paginates/pages (count @*results-all))]
+
      (when current-repo
      (when current-repo
 
 
        ;; all pages
        ;; all pages
@@ -836,147 +835,153 @@
                                       [idx (boolean (get @*checks idx))])))
                                       [idx (boolean (get @*checks idx))])))
            (reset! *results pages)))
            (reset! *results pages)))
 
 
-       [[:div.actions
-         {:class (util/classnames [{:has-selected (or (nil? @*indeterminate)
-                                                      (not= 0 @*indeterminate))}])}
-         [:div.l.flex.items-center
-          [:div.actions-wrap
-           (ui/button
-             [(ui/icon "trash") (t :delete)]
-             :on-click (fn []
-                         (let [selected (filter (fn [[_ v]] v) @*checks)
-                               selected (and (seq selected)
-                                             (into #{} (for [[k _] selected] k)))]
-                           (when-let [pages (and selected (filter #(contains? selected (:block/idx %)) @*results))]
-                             (state/set-modal! (batch-delete-dialog pages false #(do
-                                                                                   (reset! *checks nil)
-                                                                                   (refresh-pages)))))))
-             :small? true)]
-
-          [:div.search-wrap.flex.items-center.pl-2
-           (let [search-fn (fn []
+       (let [has-prev? (> @*current-page 1)
+             has-next? (not= @*current-page total-pages)]
+         [:div
+         [:div.actions
+          {:class (util/classnames [{:has-selected (or (nil? @*indeterminate)
+                                                       (not= 0 @*indeterminate))}])}
+          [:div.l.flex.items-center
+           [:div.actions-wrap
+            (ui/button
+              [(ui/icon "trash" {:style {:font-size 15}}) (t :delete)]
+              :on-click (fn []
+                          (let [selected (filter (fn [[_ v]] v) @*checks)
+                                selected (and (seq selected)
+                                              (into #{} (for [[k _] selected] k)))]
+                            (when-let [pages (and selected (filter #(contains? selected (:block/idx %)) @*results))]
+                              (state/set-modal! (batch-delete-dialog pages false #(do
+                                                                                    (reset! *checks nil)
+                                                                                    (refresh-pages)))))))
+              :class "fade-link"
+              :small? true)]
+
+           [:div.search-wrap.flex.items-center.pl-2
+            (let [search-fn (fn []
+                              (let [^js input (rum/deref *search-input)]
+                                (search-key (.-value input))
+                                (reset! *current-page 1)))
+                  reset-fn (fn []
                              (let [^js input (rum/deref *search-input)]
                              (let [^js input (rum/deref *search-input)]
-                               (search-key (.-value input))
-                               (reset! *current-page 1)))
-                 reset-fn (fn []
-                            (let [^js input (rum/deref *search-input)]
-                              (set! (.-value input) "")
-                              (reset! *search-key nil)))]
-
-             [(ui/button (ui/icon "search")
-                :on-click search-fn
-                :small? true)
-              [:input.form-input {:placeholder   (t :search/page-names)
-                                  :on-key-up     (fn [^js e]
-                                                   (let [^js target (.-target e)]
-                                                     (if (string/blank? (.-value target))
-                                                       (reset! *search-key nil)
-                                                       (cond
-                                                         (= 13 (.-keyCode e)) (search-fn)
-                                                         (= 27 (.-keyCode e)) (reset-fn)))))
-                                  :ref           *search-input
-                                  :default-value ""}]
-
-              (when (not (string/blank? @*search-key))
-                [:a.cancel {:on-click reset-fn}
-                 (ui/icon "x")])])]]
-
-         [:div.r.flex.items-center.justify-between
-          (let [orphaned-pages (model/get-orphaned-pages {})
-                orphaned-pages? (seq orphaned-pages)]
-            [:a.ml-1.pr-2.opacity-70.hover:opacity-100
-             {:on-click (fn []
-                          (if orphaned-pages?
-                            (state/set-modal!
-                             (batch-delete-dialog
-                              orphaned-pages  true
-                              #(do
-                                 (reset! *checks nil)
-                                 (refresh-pages))))
-                            (notification/show! "Congratulations, no orphaned pages in your graph!" :success)))}
-             [:span
-              (ui/icon "file-x")
-              [:span.ml-1 (t :remove-orphaned-pages)]]])
-
-          [:a.ml-1.pr-2.opacity-70.hover:opacity-100 {:href (rfe/href :all-files)}
-           [:span
-            (ui/icon "files")
-            [:span.ml-1 (t :all-files)]]]
-
-          [:div
-           (ui/tippy
-            {:html  [:small (str (t :page/show-journals) " ?")]
-             :arrow true}
-            [:a.button.journal
-             {:class    (util/classnames [{:active (boolean @*journal?)}])
-              :on-click #(reset! *journal? (not @*journal?))}
-             (ui/icon "calendar")])]
-
-          [:div.paginates
-           [:span.flex.items-center.opacity-60.text-sm
-            [:span.pr-1 (t :paginates/pages (count @*results-all))]]
-           [:span.flex.items-center
-            {:class (util/classnames [{:is-first (= 1 @*current-page)
-                                       :is-last  (= @*current-page total-pages)}])}
-            [:a.py-4.pr-2 {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))]
-            [:span.opacity-30 (str @*current-page "/" total-pages)]
-            [:a.py-4.pl-2 {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")]]]]]
-
-        [:table.table-auto.cp__all_pages_table
-         [:thead
-          [:tr
-           [:th.selector
-            (checkbox-opt "all-pages-select-all"
-                          (= 1 @*indeterminate)
-                          {:on-change     (fn []
-                                            (let [indeterminate? (= -1 @*indeterminate)
-                                                  all? (= 1 @*indeterminate)]
-                                              (doseq [{:block/keys [idx]} @*results]
-                                                (swap! *checks assoc idx (or indeterminate? (not all?))))))
-                           :indeterminate (= -1 @*indeterminate)})]
-
-           (sortable-title (t :block/name) :block/name *sort-by-item *desc?)
-           (when-not mobile?
-             [(sortable-title (t :page/backlinks) :block/backlinks *sort-by-item *desc?)
-              (sortable-title (t :page/created-at) :block/created-at *sort-by-item *desc?)
-              (sortable-title (t :page/updated-at) :block/updated-at *sort-by-item *desc?)])]]
-
-         [:tbody
-          (for [{:block/keys [idx name created-at updated-at backlinks] :as page} @*results]
-            (when-not (string/blank? name)
-              [:tr {:key name}
-               [:td.selector
-                (checkbox-opt (str "label-" idx)
-                              (get @*checks idx)
-                              {:on-change (fn []
-                                            (swap! *checks update idx not))})]
-
-               [:td.name [:a {:on-click (fn [e]
-                                          (let [repo (state/get-current-repo)]
-                                            (when (gobj/get e "shiftKey")
-                                              (state/sidebar-add-block!
-                                               repo
-                                               (:db/id page)
-                                               :page))))
-                              :href     (rfe/href :page {:name (:block/name page)})}
-                          (component-block/page-cp {} page)]]
-
-               (when-not mobile?
-                 [:td.backlinks [:span backlinks]])
-
-               (when-not mobile?
-                 [:td.created-at [:span (if created-at
-                                          (date/int->local-time-2 created-at)
-                                          "Unknown")]])
-               (when-not mobile?
-                 [:td.updated-at [:span (if updated-at
-                                          (date/int->local-time-2 updated-at)
-                                          "Unknown")]])]))]]
-
-        [:div.paginates
-         [:span]
-         [:span.flex.items-center
-          {:class (util/classnames [{:is-first (= 1 @*current-page)
-                                     :is-last  (= @*current-page total-pages)}])}
-          [:a.py-4.text-sm {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))]
-          [:a.py-4.pl-2.text-sm {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")]]]])]))
+                               (set! (.-value input) "")
+                               (reset! *search-key nil)))]
+
+              [(ui/button (ui/icon "search")
+                 :on-click search-fn
+                 :small? true)
+               [:input.form-input {:placeholder   (t :search/page-names)
+                                   :on-key-up     (fn [^js e]
+                                                    (let [^js target (.-target e)]
+                                                      (if (string/blank? (.-value target))
+                                                        (reset! *search-key nil)
+                                                        (cond
+                                                          (= 13 (.-keyCode e)) (search-fn)
+                                                          (= 27 (.-keyCode e)) (reset-fn)))))
+                                   :ref           *search-input
+                                   :default-value ""}]
+
+               (when (not (string/blank? @*search-key))
+                 [:a.cancel {:on-click reset-fn}
+                  (ui/icon "x")])])]]
+
+          [:div.r.flex.items-center.justify-between
+           [:div
+            (ui/tippy
+             {:html  [:small (str (t :page/show-journals) " ?")]
+              :arrow true}
+             [:a.button.journal
+              {:class    (util/classnames [{:active (boolean @*journal?)}])
+               :on-click #(reset! *journal? (not @*journal?))}
+              (ui/icon "calendar")])]
+
+           [:div.paginates
+            [:span.flex.items-center
+             {:class (util/classnames [{:is-first (= 1 @*current-page)
+                                        :is-last  (= @*current-page total-pages)}])}
+             (when has-prev?
+               [:a.py-4.pr-2.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+             [:span.opacity-60 (str @*current-page "/" total-pages)]
+             (when has-next?
+               [:a.py-4.pl-2.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]
+
+           (ui/dropdown-with-links
+            (fn [{:keys [toggle-fn]}]
+              [:a.button.fade-link
+               {:on-click toggle-fn}
+               (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
+            [{:title (t :remove-orphaned-pages)
+              :options {:on-click (fn []
+                                    (let [orphaned-pages (model/get-orphaned-pages {})
+                                          orphaned-pages? (seq orphaned-pages)]
+                                      (if orphaned-pages?
+                                        (state/set-modal!
+                                         (batch-delete-dialog
+                                          orphaned-pages  true
+                                          #(do
+                                             (reset! *checks nil)
+                                             (refresh-pages))))
+                                        (notification/show! "Congratulations, no orphaned pages in your graph!" :success))))}
+              :icon (ui/icon "file-x")}
+             {:title (t :all-files)
+              :options {:href (rfe/href :all-files)}
+              :icon (ui/icon "files")}]
+            {})]]
+
+         [:table.table-auto.cp__all_pages_table
+          [:thead
+           [:tr
+            [:th.selector
+             (checkbox-opt "all-pages-select-all"
+                           (= 1 @*indeterminate)
+                           {:on-change     (fn []
+                                             (let [indeterminate? (= -1 @*indeterminate)
+                                                   all? (= 1 @*indeterminate)]
+                                               (doseq [{:block/keys [idx]} @*results]
+                                                 (swap! *checks assoc idx (or indeterminate? (not all?))))))
+                            :indeterminate (= -1 @*indeterminate)})]
+
+            (sortable-title (t :block/name) :block/name *sort-by-item *desc?)
+            (when-not mobile?
+              [(sortable-title (t :page/backlinks) :block/backlinks *sort-by-item *desc?)
+               (sortable-title (t :page/created-at) :block/created-at *sort-by-item *desc?)
+               (sortable-title (t :page/updated-at) :block/updated-at *sort-by-item *desc?)])]]
+
+          [:tbody
+           (for [{:block/keys [idx name created-at updated-at backlinks] :as page} @*results]
+             (when-not (string/blank? name)
+               [:tr {:key name}
+                [:td.selector
+                 (checkbox-opt (str "label-" idx)
+                               (get @*checks idx)
+                               {:on-change (fn []
+                                             (swap! *checks update idx not))})]
+
+                [:td.name [:a {:on-click (fn [e]
+                                           (let [repo (state/get-current-repo)]
+                                             (when (gobj/get e "shiftKey")
+                                               (state/sidebar-add-block!
+                                                repo
+                                                (:db/id page)
+                                                :page))))
+                               :href     (rfe/href :page {:name (:block/name page)})}
+                           (component-block/page-cp {} page)]]
+
+                (when-not mobile?
+                  [:td.backlinks [:span backlinks]])
+
+                (when-not mobile?
+                  [:td.created-at [:span (if created-at
+                                           (date/int->local-time-2 created-at)
+                                           "Unknown")]])
+                (when-not mobile?
+                  [:td.updated-at [:span (if updated-at
+                                           (date/int->local-time-2 updated-at)
+                                           "Unknown")]])]))]]
+
+         [:div.paginates
+          [:span]
+          [:span.flex.items-center
+           (when has-prev?
+             [:a.py-4.text-sm.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+           (when has-next?
+             [:a.py-4.pl-2.text-sm.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]]))]))

+ 15 - 15
src/main/frontend/components/page.css

@@ -101,7 +101,7 @@
     position: sticky;
     position: sticky;
     background-color: var(--ls-primary-background-color);
     background-color: var(--ls-primary-background-color);
     white-space: nowrap;
     white-space: nowrap;
-    top: -1px;
+    top: -18px;
     padding-bottom: 10px;
     padding-bottom: 10px;
     z-index: 1;
     z-index: 1;
 
 
@@ -119,7 +119,6 @@
       font-size: 16px;
       font-size: 16px;
       display: inline-block;
       display: inline-block;
       position: relative;
       position: relative;
-      bottom: -1px;
     }
     }
 
 
     .l {
     .l {
@@ -136,7 +135,7 @@
 
 
       a.journal {
       a.journal {
         color: var(--ls-primary-text-color);
         color: var(--ls-primary-text-color);
-        opacity: .3;
+        margin-top: 1px;
 
 
         &.active {
         &.active {
           opacity: 1;
           opacity: 1;
@@ -148,7 +147,7 @@
         display: flex;
         display: flex;
         align-items: center;
         align-items: center;
         justify-content: center;
         justify-content: center;
-        margin-right: 5px;
+        margin-right: 3px;
       }
       }
     }
     }
 
 
@@ -195,7 +194,7 @@
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
     justify-content: space-between;
     justify-content: space-between;
-    padding: 0 5px;
+    padding: 0 5px 0 0;
 
 
     > span {
     > span {
       color: var(--ls-primary-text-color);
       color: var(--ls-primary-text-color);
@@ -209,12 +208,6 @@
           }
           }
         }
         }
       }
       }
-
-      &.is-first > a:first-child,
-      &.is-last > a:last-child {
-        pointer-events: none;
-        opacity: .3;
-      }
     }
     }
   }
   }
 }
 }
@@ -257,10 +250,10 @@
 
 
 .ls-page-title {
 .ls-page-title {
   @apply rounded-sm;
   @apply rounded-sm;
-  
+
   padding: 5px 8px;
   padding: 5px 8px;
   margin: 0 -6px;
   margin: 0 -6px;
-  
+
   &.title {
   &.title {
     margin-bottom: 12px;
     margin-bottom: 12px;
   }
   }
@@ -274,11 +267,18 @@
   }
   }
 }
 }
 
 
+a.page-title {
+  padding: 0 8px;
+  margin-left: -8px;
+  transition: none;
+  display: block;
+}
+
 html.is-native-android,
 html.is-native-android,
 html.is-native-ipad,
 html.is-native-ipad,
 html.is-native-iphone,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
 html.is-native-iphone-without-notch {
-    
+
     .ls-page-title {
     .ls-page-title {
         margin: 0px 0px 24px -15px;
         margin: 0px 0px 24px -15px;
         padding: 0px;
         padding: 0px;
@@ -383,4 +383,4 @@ html.is-native-ios {
   display: inline-block;
   display: inline-block;
   line-height: normal;
   line-height: normal;
   background-color: var(--ls-quaternary-background-color);
   background-color: var(--ls-quaternary-background-color);
- }
+ }

+ 2 - 1
src/main/frontend/components/reference.cljs

@@ -123,7 +123,8 @@
                                   references (->> (concat ref-pages references)
                                   references (->> (concat ref-pages references)
                                                   (remove nil?)
                                                   (remove nil?)
                                                   (distinct))]
                                                   (distinct))]
-                              (state/set-modal! (filter-dialog filters-atom references page-name))))}
+                              (state/set-modal! (filter-dialog filters-atom references page-name)
+                                                {:center? true})))}
                (ui/icon "filter" {:class (cond
                (ui/icon "filter" {:class (cond
                                            (empty? filter-state)
                                            (empty? filter-state)
                                            ""
                                            ""

+ 5 - 5
src/main/frontend/components/repo.cljs

@@ -14,7 +14,7 @@
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
-            [logseq.graph-parser.text :as text]
+            [frontend.util.text :as text-util]
             [promesa.core :as p]
             [promesa.core :as p]
             [electron.ipc :as ipc]
             [electron.ipc :as ipc]
             [goog.object :as gobj]
             [goog.object :as gobj]
@@ -54,7 +54,7 @@
             [:div.flex.justify-between.mb-4 {:key id}
             [:div.flex.justify-between.mb-4 {:key id}
              (if local?
              (if local?
                (let [local-dir (config/get-local-dir url)
                (let [local-dir (config/get-local-dir url)
-                     graph-name (text/get-graph-name-from-path local-dir)]
+                     graph-name (text-util/get-graph-name-from-path local-dir)]
                  [:a {:title local-dir
                  [:a {:title local-dir
                       :on-click #(state/pub-event! [:graph/switch url])}
                       :on-click #(state/pub-event! [:graph/switch url])}
                   graph-name])
                   graph-name])
@@ -89,7 +89,7 @@
         repo-links (mapv
         repo-links (mapv
                     (fn [{:keys [url]}]
                     (fn [{:keys [url]}]
                       (let [repo-path (db/get-repo-name url)
                       (let [repo-path (db/get-repo-name url)
-                            short-repo-name (text/get-graph-name-from-path repo-path)]
+                            short-repo-name (text-util/get-graph-name-from-path repo-path)]
                         {:title short-repo-name
                         {:title short-repo-name
                          :hover-detail repo-path ;; show full path on hover
                          :hover-detail repo-path ;; show full path on hover
                          :options {:class "ml-1"
                          :options {:class "ml-1"
@@ -148,12 +148,12 @@
             render-content (fn [{:keys [toggle-fn]}]
             render-content (fn [{:keys [toggle-fn]}]
                              (let [repo-path (db/get-repo-name current-repo)
                              (let [repo-path (db/get-repo-name current-repo)
                                    short-repo-name (db/get-short-repo-name repo-path)]
                                    short-repo-name (db/get-short-repo-name repo-path)]
-                               [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+                               [:a.item.group.flex.items-center.px-1.py-2.text-sm.font-medium.rounded-md
                                 {:on-click (fn []
                                 {:on-click (fn []
                                              (check-multiple-windows? state)
                                              (check-multiple-windows? state)
                                              (toggle-fn))
                                              (toggle-fn))
                                  :title repo-path} ;; show full path on hover
                                  :title repo-path} ;; show full path on hover
-                                (ui/icon "database mr-3" {:style {:font-size 20} :id "database-icon"})
+                                (ui/icon "database mr-2" {:style {:font-size 16} :id "database-icon"})
                                 [:div.graphs
                                 [:div.graphs
                                  [:span#repo-switch.block.pr-2.whitespace-nowrap
                                  [:span#repo-switch.block.pr-2.whitespace-nowrap
                                   [:span [:span#repo-name.font-medium short-repo-name]]
                                   [:span [:span#repo-name.font-medium short-repo-name]]

+ 1 - 1
src/main/frontend/components/right_sidebar.cljs

@@ -122,7 +122,7 @@
   (let [item (build-sidebar-item repo idx db-id block-type)]
   (let [item (build-sidebar-item repo idx db-id block-type)]
     (when item
     (when item
       (let [collapse? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
       (let [collapse? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
-        [:div.sidebar-item.content.color-level.px-4.shadow-lg
+        [:div.sidebar-item.content.color-level.px-4.shadow-md
          (let [[title component] item]
          (let [[title component] item]
            [:div.flex.flex-col
            [:div.flex.flex-col
             [:div.flex.flex-row.justify-between
             [:div.flex.flex-row.justify-between

+ 3 - 3
src/main/frontend/components/select.cljs

@@ -9,8 +9,8 @@
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
+            [frontend.util.text :as text-util]
             [frontend.db :as db]
             [frontend.db :as db]
-            [logseq.graph-parser.text :as text]
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.repo :as repo-handler]
@@ -76,7 +76,7 @@
                            (or (config/demo-graph? url)
                            (or (config/demo-graph? url)
                                (= url (state/get-current-repo)))))
                                (= url (state/get-current-repo)))))
                  (map (fn [{:keys [url]}]
                  (map (fn [{:keys [url]}]
-                        {:value (text/get-graph-name-from-path
+                        {:value (text-util/get-graph-name-from-path
                                  ;; TODO: Use helper when a common one is refactored
                                  ;; TODO: Use helper when a common one is refactored
                                  ;; from components.repo
                                  ;; from components.repo
                                  (if (config/local-db? url)
                                  (if (config/local-db? url)
@@ -99,7 +99,7 @@
                      (remove (fn [{:keys [url]}]
                      (remove (fn [{:keys [url]}]
                                (config/demo-graph? url)))
                                (config/demo-graph? url)))
                      (map (fn [{:keys [url] :as original-graph}]
                      (map (fn [{:keys [url] :as original-graph}]
-                            {:value (text/get-graph-name-from-path
+                            {:value (text-util/get-graph-name-from-path
                                      ;; TODO: Use helper when a common one is refactored
                                      ;; TODO: Use helper when a common one is refactored
                                      ;; from components.repo
                                      ;; from components.repo
                                      (if (config/local-db? url)
                                      (if (config/local-db? url)

+ 9 - 0
src/main/frontend/components/settings.cljs

@@ -151,6 +151,14 @@
      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
      :-for         "customize_css"}))
      :-for         "customize_css"}))
 
 
+(defn edit-export-css []
+  (row-with-button-action
+   {:left-label   (t :settings-page/export-theme)
+    :button-label (t :settings-page/edit-export-css)
+    :href         (rfe/href :file {:path (config/get-export-css-path)})
+    :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+    :-for         "customize_css"}))
+
 (defn show-brackets-row [t show-brackets?]
 (defn show-brackets-row [t show-brackets?]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
    [:label.block.text-sm.font-medium.leading-5.opacity-70
    [:label.block.text-sm.font-medium.leading-5.opacity-70
@@ -518,6 +526,7 @@
      (theme-modes-row t switch-theme system-theme? dark?)
      (theme-modes-row t switch-theme system-theme? dark?)
      (when current-repo (edit-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
      (when current-repo (edit-custom-css))
+     (when current-repo (edit-export-css))
      (keyboard-shortcuts-row t)]))
      (keyboard-shortcuts-row t)]))
 
 
 (rum/defcs settings-editor < rum/reactive
 (rum/defcs settings-editor < rum/reactive

+ 117 - 72
src/main/frontend/components/sidebar.cljs

@@ -19,22 +19,25 @@
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.srs :as srs]
             [frontend.extensions.srs :as srs]
+            [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.handler.common :as common-handler]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
+            [frontend.mobile.action-bar :as action-bar]
             [frontend.mobile.footer :as footer]
             [frontend.mobile.footer :as footer]
-            [frontend.mobile.util :as mobile-util]
             [frontend.mobile.mobile-bar :refer [mobile-bar]]
             [frontend.mobile.mobile-bar :refer [mobile-bar]]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
+            [frontend.util.cursor :as cursor]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
+            [react-draggable]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
             [rum.core :as rum]))
 
 
@@ -43,16 +46,17 @@
 
 
   [:div.nav-content-item.is-expand
   [:div.nav-content-item.is-expand
    {:class class}
    {:class class}
-   [:div.header.items-center.mb-1
-    {:on-click (fn [^js/MouseEvent e]
-                 (let [^js target (.-target e)
-                       ^js parent (.closest target ".nav-content-item")]
-                   (.toggle (.-classList parent) "is-expand")))}
-
-    [:div.font-medium.fade-link name]
-    [:span
-     [:a.more svg/arrow-down-v2]]]
-   [:div.bd child]])
+   [:div.nav-content-item-inner
+    [:div.header.items-center.mb-1
+     {:on-click (fn [^js/MouseEvent e]
+                  (let [^js target (.-target e)
+                        ^js parent (.closest target ".nav-content-item")]
+                    (.toggle (.-classList parent) "is-expand")))}
+
+     [:div.font-medium.fade-link name]
+     [:span
+      [:a.more svg/arrow-down-v2]]]
+    [:div.bd child]]])
 
 
 (defn- delta-y
 (defn- delta-y
   [e]
   [e]
@@ -81,7 +85,7 @@
      (pdf-assets/fix-local-asset-filename original-name)]))
      (pdf-assets/fix-local-asset-filename original-name)]))
 
 
 (defn get-page-icon [page-entity]
 (defn get-page-icon [page-entity]
-  (let [default-icon "◦"
+  (let [default-icon (ui/icon "file-text")
         from-properties (get-in (into {} page-entity) [:block/properties :icon])]
         from-properties (get-in (into {} page-entity) [:block/properties :icon])]
     (or
     (or
      (when (not= from-properties "") from-properties)
      (when (not= from-properties "") from-properties)
@@ -96,6 +100,7 @@
         target (state/sub :favorites/dragging)]
         target (state/sub :favorites/dragging)]
     [:li.favorite-item
     [:li.favorite-item
      {:key name
      {:key name
+      :title name
       :data-ref name
       :data-ref name
       :class (if (and target @dragging-over (not= target @dragging-over))
       :class (if (and target @dragging-over (not= target @dragging-over))
                "dragging-target"
                "dragging-target"
@@ -120,10 +125,9 @@
 (rum/defc favorites < rum/reactive
 (rum/defc favorites < rum/reactive
   [t]
   [t]
   (nav-content-item
   (nav-content-item
-   [:a.flex.items-center.text-sm.font-medium.rounded-md
-    (ui/icon "star mr-1" {:style {:font-size 18}})
-    [:span.flex-1.ml-1 {:style {:padding-top 2}}
-     (t :left-side-bar/nav-favorites)]]
+   [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
+    (ui/icon "star mr-1" {:style {:font-size 16}})
+    [:span.flex-1.ml-1 (string/upper-case (t :left-side-bar/nav-favorites))]]
 
 
    {:class "favorites"
    {:class "favorites"
     :edit-fn
     :edit-fn
@@ -145,10 +149,10 @@
 (rum/defc recent-pages < rum/reactive db-mixins/query
 (rum/defc recent-pages < rum/reactive db-mixins/query
   [t]
   [t]
   (nav-content-item
   (nav-content-item
-   [:a.flex.items-center.text-sm.font-medium.rounded-md
-    (ui/icon "history mr-2" {:style {:font-size 18}})
-    [:span.flex-1 {:style {:padding-top 2}}
-     (t :left-side-bar/nav-recent-pages)]]
+   [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
+    (ui/icon "history mr-2" {:style {:font-size 16}})
+    [:span.flex-1
+     (string/upper-case (t :left-side-bar/nav-recent-pages))]]
 
 
    {:class "recent"}
    {:class "recent"}
 
 
@@ -164,6 +168,7 @@
         (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
         (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
           [:li.recent-item.select-none
           [:li.recent-item.select-none
            {:key name
            {:key name
+            :title name
             :data-ref name}
             :data-ref name}
            (page-name name (get-page-icon entity))]))])))
            (page-name name (get-page-icon entity))]))])))
 
 
@@ -171,10 +176,12 @@
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (srs/update-cards-due-count!)
                 (srs/update-cards-due-count!)
                 state)}
                 state)}
-  [state]
+  [_state srs-open?]
   (let [num (state/sub :srs/cards-due-count)]
   (let [num (state/sub :srs/cards-due-count)]
-    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click #(state/pub-event! [:modal/show-cards])}
-     (ui/icon "infinity mr-3" {:style {:font-size 20}})
+    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+     {:class (util/classnames [{:active srs-open?}])
+      :on-click #(state/pub-event! [:modal/show-cards])}
+     (ui/icon "infinity")
      [:span.flex-1 (t :right-side-bar/flashcards)]
      [:span.flex-1 (t :right-side-bar/flashcards)]
      (when (and num (not (zero? num)))
      (when (and num (not (zero? num)))
        [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
        [:span.ml-3.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
@@ -195,18 +202,21 @@
     class :class
     class :class
     title :title
     title :title
     icon :icon
     icon :icon
+    active :active
     href :href}]
     href :href}]
   [:div
   [:div
    {:class class}
    {:class class}
    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
    [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
     {:on-click on-click-handler
     {:on-click on-click-handler
+     :class (when active "active")
      :href href}
      :href href}
-    (ui/icon (str icon " mr-3") {:style {:font-size 20}})
+    (ui/icon (str icon))
     [:span.flex-1 title]]])
     [:span.flex-1 title]]])
 
 
 (rum/defc sidebar-nav
 (rum/defc sidebar-nav
-  [_route-match close-modal-fn left-sidebar-open?]
-  (let [default-home (get-default-home-if-valid)]
+  [route-match close-modal-fn left-sidebar-open? srs-open?]
+  (let [default-home (get-default-home-if-valid)
+        route-name (get-in route-match [:data :name])]
 
 
     [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
     [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
      {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
      {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
@@ -214,37 +224,44 @@
                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
                      (close-modal-fn)))}
                      (close-modal-fn)))}
      [:div.flex.flex-col.pb-4.wrap
      [:div.flex.flex-col.pb-4.wrap
-      [:nav.px-2.space-y-1 {:aria-label "Sidebar"}
+      [:nav.px-4.pt-1.space-y-1 {:aria-label "Sidebar"}
        (repo/repos-dropdown)
        (repo/repos-dropdown)
 
 
        [:div.nav-header
        [:div.nav-header
-        (if (:page default-home)
+        (if-let [page (:page default-home)]
           (sidebar-item
           (sidebar-item
-           {:class            "home-nav"
-            :title            (:page default-home)
-            :on-click-handler route-handler/redirect-to-home!
-            :icon             "home"})
+            {:class            "home-nav"
+             :title            page
+             :on-click-handler route-handler/redirect-to-home!
+             :active           (and (not srs-open?)
+                                    (= route-name :page)
+                                    (= page (get-in route-match [:path-params :name])))
+             :icon             "home"})
           (sidebar-item
           (sidebar-item
-           {:class            "journals-nav"
-            :title            (t :left-side-bar/journals)
-            :on-click-handler route-handler/go-to-journals!
-            :icon             "calendar"}))
+            {:class            "journals-nav"
+             :active           (and (not srs-open?)
+                                 (or (= route-name :all-journals) (= route-name :home)))
+             :title            (t :left-side-bar/journals)
+             :on-click-handler route-handler/go-to-journals!
+             :icon             "calendar"}))
 
 
         [:div.flashcards-nav
         [:div.flashcards-nav
-         (flashcards)]
+         (flashcards srs-open?)]
 
 
         (sidebar-item
         (sidebar-item
-         {:class "graph-view-nav"
-          :title (t :right-side-bar/graph-view)
-          :href  (rfe/href :graph)
-          :icon  "hierarchy"})
+          {:class  "graph-view-nav"
+           :title  (t :right-side-bar/graph-view)
+           :href   (rfe/href :graph)
+           :active (and (not srs-open?) (= route-name :graph))
+           :icon   "hierarchy"})
 
 
         (sidebar-item
         (sidebar-item
-         {:class "all-pages-nav"
-          :title (t :right-side-bar/all-pages)
-          :href  (rfe/href :all-pages)
-          :icon  "files"})
-        
+          {:class  "all-pages-nav"
+           :title  (t :right-side-bar/all-pages)
+           :href   (rfe/href :all-pages)
+           :active (and (not srs-open?) (= route-name :all-pages))
+           :icon   "files"})
+           
         (sidebar-item
         (sidebar-item
          {:class "whiteboard"
          {:class "whiteboard"
           :title "Whiteboards"
           :title "Whiteboards"
@@ -255,27 +272,45 @@
 
 
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
       (when (and left-sidebar-open? (not config/publishing?)) (recent-pages t))
 
 
-      [:nav.px-2 {:aria-label "Sidebar"
-                  :class      "new-page"}
-       (when-not config/publishing?
-         [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
-          {:on-click (fn []
-                       (and (util/sm-breakpoint?)
-                            (state/toggle-left-sidebar!))
-                       (state/pub-event! [:go/search]))}
-          (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-          [:span.flex-1 (t :right-side-bar/new-page)]])]]]))
+      (when-not (mobile-util/native-platform?)
+       [:nav.px-2 {:aria-label "Sidebar"
+                   :class      "new-page"}
+        (when-not config/publishing?
+          [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
+           {:on-click (fn []
+                        (and (util/sm-breakpoint?)
+                             (state/toggle-left-sidebar!))
+                        (state/pub-event! [:go/search]))}
+           (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+           [:span.flex-1 (t :right-side-bar/new-page)]])])]]))
 
 
 (rum/defc left-sidebar < rum/reactive
 (rum/defc left-sidebar < rum/reactive
   [{:keys [left-sidebar-open? route-match]}]
   [{:keys [left-sidebar-open? route-match]}]
-  (let [close-fn #(state/set-left-sidebar-open! false)]
+  (let [close-fn #(state/set-left-sidebar-open! false)
+        srs-open? (= :srs (state/sub :modal/id))]
     [:div#left-sidebar.cp__sidebar-left-layout
     [:div#left-sidebar.cp__sidebar-left-layout
      {:class (util/classnames [{:is-open left-sidebar-open?}])}
      {:class (util/classnames [{:is-open left-sidebar-open?}])}
 
 
      ;; sidebar contents
      ;; sidebar contents
-     (sidebar-nav route-match close-fn left-sidebar-open?)
+     (sidebar-nav route-match close-fn left-sidebar-open? srs-open?)
      [:span.shade-mask {:on-click close-fn}]]))
      [:span.shade-mask {:on-click close-fn}]]))
 
 
+(rum/defc recording-bar
+  []
+  [:> react-draggable
+   {:onStart (fn [_event]
+               (when-let [pos (some-> (state/get-input) cursor/pos)]
+                 (state/set-editor-last-pos! pos)))
+    :onStop (fn [_event]
+              (when-let [block (get-in @state/state [:editor/block :block/uuid])]
+                (editor-handler/edit-block! block :max (:block/uuid block))
+                (when-let [input (state/get-input)]
+                  (when-let [saved-cursor (state/get-editor-last-pos)]
+                    (cursor/move-cursor-to input saved-cursor)))))}
+   [:div#audio-record-toolbar
+    {:style {:bottom (+ @util/keyboard-height 45)}}
+    (footer/audio-record-cp)]])
+
 (rum/defc main <
 (rum/defc main <
   {:did-mount (fn [state]
   {:did-mount (fn [state]
                 (when-let [element (gdom/getElement "main-content-container")]
                 (when-let [element (gdom/getElement "main-content-container")]
@@ -288,7 +323,7 @@
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
                                 (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
                   (common-handler/listen-to-scroll! element))
                   (common-handler/listen-to-scroll! element))
                 state)}
                 state)}
-  [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content]}]
+  [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
   (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
         onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
                                   (not config/publishing?)
                                   (not config/publishing?)
@@ -303,18 +338,24 @@
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
       {:data-is-margin-less-pages margin-less-pages?}
       {:data-is-margin-less-pages margin-less-pages?}
 
 
+      (when show-action-bar?
+        (action-bar/action-bar))
+      
       [:div.cp__sidebar-main-content
       [:div.cp__sidebar-main-content
        {:data-is-margin-less-pages margin-less-pages?
        {:data-is-margin-less-pages margin-less-pages?
         :data-is-full-width        (or margin-less-pages?
         :data-is-full-width        (or margin-less-pages?
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
 
 
+       (when show-recording-bar?
+         (recording-bar))
+
        (mobile-bar)
        (mobile-bar)
        (footer/footer)
        (footer/footer)
-
+       
        (when (and (not (mobile-util/native-platform?))
        (when (and (not (mobile-util/native-platform?))
                   (contains? #{:page :home} route-name))
                   (contains? #{:page :home} route-name))
          (widgets/demo-graph-alert))
          (widgets/demo-graph-alert))
-
+       
        (cond
        (cond
          (not indexeddb-support?)
          (not indexeddb-support?)
          nil
          nil
@@ -384,10 +425,9 @@
                                               [page :page])]
                                               [page :page])]
                      (state/sidebar-add-block! current-repo db-id block-type)))
                      (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
                  (reset! sidebar-inited? true))))
-           state)
-   :did-mount (fn [state]
-                (state/set-state! :mobile/show-tabbar? true)
-                state)}
+           (when (state/mobile?)
+                  (state/set-state! :mobile/show-tabbar? true))
+           state)}
   []
   []
   (let [default-home (get-default-home-if-valid)
   (let [default-home (get-default-home-if-valid)
         current-repo (state/sub :git/current-repo)
         current-repo (state/sub :git/current-repo)
@@ -466,10 +506,11 @@
   []
   []
   (when-not (state/sub :ui/sidebar-open?)
   (when-not (state/sub :ui/sidebar-open?)
     [:div.cp__sidebar-help-btn
     [:div.cp__sidebar-help-btn
-     {:title (t :help-shortcut-title)
-      :on-click (fn []
-                  (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
-     "?"]))
+     [:div.inner
+      {:title    (t :help-shortcut-title)
+       :on-click (fn []
+                   (state/sidebar-add-block! (state/get-current-repo) "help" :help))}
+      "?"]]))
 
 
 (defn- hide-context-menu-and-clear-selection
 (defn- hide-context-menu-and-clear-selection
   [e]
   [e]
@@ -518,7 +559,9 @@
         home? (= :home route-name)
         home? (= :home route-name)
         edit? (:editor/editing? @state/state)
         edit? (:editor/editing? @state/state)
         default-home (get-default-home-if-valid)
         default-home (get-default-home-if-valid)
-        logged? (user-handler/logged-in?)]
+        logged? (user-handler/logged-in?)
+        show-action-bar? (state/sub :mobile/show-action-bar?)
+        show-recording-bar? (state/sub :mobile/show-recording-bar?)]
     (theme/container
     (theme/container
      {:t             t
      {:t             t
       :theme         theme
       :theme         theme
@@ -560,7 +603,9 @@
                :indexeddb-support?  indexeddb-support?
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :light?              light?
                :db-restoring?       db-restoring?
                :db-restoring?       db-restoring?
-               :main-content        main-content})]
+               :main-content        main-content
+               :show-action-bar?    show-action-bar?
+               :show-recording-bar? show-recording-bar?})]
 
 
        (right-sidebar/sidebar)
        (right-sidebar/sidebar)
 
 

+ 71 - 37
src/main/frontend/components/sidebar.css

@@ -37,13 +37,13 @@
     height: 100%;
     height: 100%;
     transition: padding-left .3s;
     transition: padding-left .3s;
 
 
-    &.is-left-sidebar-open {
-        padding-left: 0;
+  &.is-left-sidebar-open {
+    padding-left: 0;
 
 
-        @screen sm {
-            padding-left: var(--ls-left-sidebar-width);
-        }
+    @screen sm {
+      padding-left: var(--ls-left-sidebar-width);
     }
     }
+  }
 }
 }
 
 
 #main-content {
 #main-content {
@@ -73,6 +73,8 @@
   transform: translateX(-100%);
   transform: translateX(-100%);
   z-index: 3;
   z-index: 3;
 
 
+  -webkit-font-smoothing: antialiased;
+
   > .wrap {
   > .wrap {
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     height: calc(100vh - var(--ls-headbar-inner-top-padding) - 50px);
     margin-top: 40px;
     margin-top: 40px;
@@ -85,36 +87,53 @@
   }
   }
 
 
   .page-icon {
   .page-icon {
-    margin-right: 8px;
-    width: 1.1em;
     text-align: center;
     text-align: center;
     display: inline-block;
     display: inline-block;
+    line-height: 1em;
     color: #aaa;
     color: #aaa;
+    padding: 0 4px 0 8px;
   }
   }
 
 
   a.item {
   a.item {
     user-select: none;
     user-select: none;
-    transition: none;
+    transition: background-color .3s;
+    margin-bottom: 2px;
+
+    > .ti {
+      font-size: 16px;
+      margin-right: 8px;
+      opacity: .6;
+      position: relative;
+      top: -1px;
+    }
 
 
-    > span {
-      margin-top: 3px;
+    &.active, &:active {
+      background-color: var(--ls-quaternary-background-color);
     }
     }
 
 
     &:hover {
     &:hover {
-      background-color: var(--ls-quaternary-background-color);
+      opacity: .8;
+
+      .ti {
+        opacity: .8;
+      }
     }
     }
   }
   }
 
 
   .nav-content-item {
   .nav-content-item {
-    padding: 18px 0 0 0;
+    margin-top: 14px;
+
+    &-inner {
+      border-radius: 8px;
+    }
 
 
-    > .header {
+    .header {
       display: flex;
       display: flex;
       justify-content: space-between;
       justify-content: space-between;
       align-items: center;
       align-items: center;
       user-select: none;
       user-select: none;
       cursor: pointer;
       cursor: pointer;
-      padding: 4px 18px;
+      padding: 4px 25px;
 
 
       > span {
       > span {
         > a {
         > a {
@@ -157,9 +176,17 @@
       &:active {
       &:active {
         opacity: .8;
         opacity: .8;
       }
       }
+
+      .wrap-th {
+        > span {
+          font-size: 11px;
+          font-weight: 600;
+          padding-top: 2px;
+        }
+      }
     }
     }
 
 
-    > .bd {
+    .bd {
       display: none;
       display: none;
 
 
       ul {
       ul {
@@ -169,12 +196,13 @@
 
 
         a {
         a {
           width: 100%;
           width: 100%;
-          padding: 2px 20px;
+          padding: 2px 18px;
           display: block;
           display: block;
           text-overflow: ellipsis;
           text-overflow: ellipsis;
           overflow: hidden;
           overflow: hidden;
           white-space: nowrap;
           white-space: nowrap;
           color: var(--ls-primary-text-color);
           color: var(--ls-primary-text-color);
+          transition: background-color .3s;
 
 
           &:hover {
           &:hover {
             background-color: var(--ls-quaternary-background-color);
             background-color: var(--ls-quaternary-background-color);
@@ -188,13 +216,13 @@
     }
     }
 
 
     &.is-expand {
     &.is-expand {
-      > .header > span > a {
+      .header > span > a {
         &:last-child {
         &:last-child {
           transform: translateY(2px) translateX(-3px);
           transform: translateY(2px) translateX(-3px);
         }
         }
       }
       }
 
 
-      > .bd {
+      .bd {
         display: block;
         display: block;
       }
       }
     }
     }
@@ -202,25 +230,30 @@
 
 
   .new-page {
   .new-page {
     position: absolute;
     position: absolute;
-    background-color: var(--ls-primary-background-color);
     bottom: 0;
     bottom: 0;
     left: 0;
     left: 0;
     width: 100%;
     width: 100%;
-    padding-bottom: 8px;
-    padding-top: 8px;
+    padding: 14px;
+
+    &-link {
+      background-color: var(--ls-primary-background-color);
+      box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
+    }
   }
   }
 
 
   @screen sm {
   @screen sm {
-    background-color: var(--ls-secondary-background-color);
-    width: var(--ls-left-sidebar-width);
     padding-top: 0;
     padding-top: 0;
+    width: var(--ls-left-sidebar-width);
+    background-color: var(--ls-secondary-background-color);
 
 
     > .wrap {
     > .wrap {
       margin-top: 50px;
       margin-top: 50px;
     }
     }
 
 
     .new-page {
     .new-page {
-      background-color: var(--ls-secondary-background-color);
+      &-link {
+        background-color: var(--ls-primary-background-color);
+      }
     }
     }
   }
   }
 }
 }
@@ -307,9 +340,9 @@
 }
 }
 
 
 .ls-wide-mode {
 .ls-wide-mode {
- .cp__sidebar-main-content {
-   max-width: var(--ls-main-content-max-width-wide);
- }
+  .cp__sidebar-main-content {
+    max-width: var(--ls-main-content-max-width-wide);
+  }
 }
 }
 
 
 html[data-theme='dark'] {
 html[data-theme='dark'] {
@@ -352,14 +385,15 @@ html[data-theme='dark'] {
   }
   }
 
 
   &-btn {
   &-btn {
-    @apply font-bold fixed bottom-4
-    rounded-full h-8 w-8 flex items-center justify-center font-bold
-    opacity-70 hover:opacity-100;
+    @apply fixed bottom-4 right-8;
 
 
-    user-select: none;
-    cursor: help;
-    right: 24px;
-    background-color: var(--ls-secondary-background-color);
+    > .inner {
+      @apply font-bold
+      rounded-full h-8 w-8 flex items-center justify-center font-bold
+      opacity-70 hover:opacity-100 select-none cursor-help;
+
+      background-color: var(--ls-secondary-background-color);
+    }
   }
   }
 }
 }
 
 
@@ -373,8 +407,8 @@ html[data-theme='dark'] {
   }
   }
 
 
   &.open {
   &.open {
-      width: var(--ls-right-sidebar-width);
-      max-width: 60vw;
+    width: var(--ls-right-sidebar-width);
+    max-width: 60vw;
   }
   }
 
 
   &-scollable {
   &-scollable {
@@ -484,5 +518,5 @@ html[data-theme='dark'] {
 }
 }
 
 
 .full-height-without-header {
 .full-height-without-header {
-    height: calc(100vh - var(--ls-headbar-height) - 4rem);
+  height: calc(100vh - var(--ls-headbar-height) - 4rem);
 }
 }

+ 2 - 2
src/main/frontend/components/svg.cljs

@@ -380,8 +380,8 @@
 
 
 (def circle-stop
 (def circle-stop
   [:svg
   [:svg
-   {:width "1.3rem"
-    :height "1.3rem"
+   {:width "20px"
+    :height "20px"
     :viewBox "0 0 512 512"
     :viewBox "0 0 512 512"
     :fill     "currentColor"}
     :fill     "currentColor"}
    [:path
    [:path

+ 14 - 38
src/main/frontend/config.cljs

@@ -5,6 +5,7 @@
             [frontend.util :as util]
             [frontend.util :as util]
             [shadow.resource :as rc]
             [shadow.resource :as rc]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.config :as gp-config]
             [frontend.mobile.util :as mobile-util]))
             [frontend.mobile.util :as mobile-util]))
 
 
 (goog-define DEV-RELEASE false)
 (goog-define DEV-RELEASE false)
@@ -63,49 +64,20 @@
     (if dev? path
     (if dev? path
         (str asset-domain path))))
         (str asset-domain path))))
 
 
-(defn text-formats
-  []
-  (let [config-formats (some->> (get-in @state/state [:config :text-formats])
-                                (map :keyword)
-                                (set))]
-    (set/union
-     config-formats
-     #{:json :org :md :yml :dat :asciidoc :rst :txt :markdown :adoc :html :js :ts :edn :clj :ml :rb :ex :erl :java :php :c :css
-       :excalidraw :tldr})))
-
 (def markup-formats
 (def markup-formats
   #{:org :md :markdown :asciidoc :adoc :rst})
   #{:org :md :markdown :asciidoc :adoc :rst})
 
 
-(defn img-formats
-  []
-  (let [config-formats (some->> (get-in @state/state [:config :image-formats])
-                                (map :keyword)
-                                (set))]
-    (set/union
-     config-formats
-     #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})))
-
 (defn doc-formats
 (defn doc-formats
   []
   []
-  (let [config-formats (some->> (get-in @state/state [:config :document-formats])
-                                (map :keyword)
-                                (set))]
-    (set/union
-     config-formats
-     #{:doc :docx :xls :xlsx :ppt :pptx :one :pdf :epub})))
+  #{:doc :docx :xls :xlsx :ppt :pptx :one :pdf :epub})
 
 
 (def audio-formats #{:mp3 :ogg :mpeg :wav :m4a :flac :wma :aac})
 (def audio-formats #{:mp3 :ogg :mpeg :wav :m4a :flac :wma :aac})
 
 
-(def media-formats (set/union (img-formats) audio-formats))
+(def media-formats (set/union (gp-config/img-formats) audio-formats))
 
 
 (def html-render-formats
 (def html-render-formats
   #{:adoc :asciidoc})
   #{:adoc :asciidoc})
 
 
-(defn supported-formats
-  []
-  (set/union (text-formats)
-             (img-formats)))
-
 (def mobile?
 (def mobile?
   (when-not util/node-test?
   (when-not util/node-test?
     (util/safe-re-find #"Mobi" js/navigator.userAgent)))
     (util/safe-re-find #"Mobi" js/navigator.userAgent)))
@@ -114,13 +86,7 @@
 
 
 (defn get-block-pattern
 (defn get-block-pattern
   [format]
   [format]
-  (let [format (or format (state/get-preferred-format))
-        format (keyword format)]
-    (case format
-      :org
-      "*"
-
-      "-")))
+  (gp-config/get-block-pattern (or format (state/get-preferred-format))))
 
 
 (defn get-hr
 (defn get-hr
   [format]
   [format]
@@ -277,6 +243,7 @@
 (defonce recycle-dir ".recycle")
 (defonce recycle-dir ".recycle")
 (def config-file "config.edn")
 (def config-file "config.edn")
 (def custom-css-file "custom.css")
 (def custom-css-file "custom.css")
+(def export-css-file "export.css")
 (def custom-js-file "custom.js")
 (def custom-js-file "custom.js")
 (def metadata-file "metadata.edn")
 (def metadata-file "metadata.edn")
 (def pages-metadata-file "pages-metadata.edn")
 (def pages-metadata-file "pages-metadata.edn")
@@ -393,6 +360,15 @@
      (get-file-path repo
      (get-file-path repo
                     (str app-name "/" custom-css-file)))))
                     (str app-name "/" custom-css-file)))))
 
 
+(defn get-export-css-path
+  ([]
+   (get-export-css-path (state/get-current-repo)))
+  ([repo]
+   (when repo
+     (get-file-path repo
+                    (str app-name "/" export-css-file)))))
+
+
 (defn get-custom-js-path
 (defn get-custom-js-path
   ([]
   ([]
    (get-custom-js-path (state/get-current-repo)))
    (get-custom-js-path (state/get-current-repo)))

+ 23 - 32
src/main/frontend/db.cljs

@@ -1,9 +1,9 @@
 (ns frontend.db
 (ns frontend.db
   (:require [clojure.core.async :as async]
   (:require [clojure.core.async :as async]
             [datascript.core :as d]
             [datascript.core :as d]
-            [frontend.db-schema :as db-schema]
+            [logseq.graph-parser.db.schema :as db-schema]
             [frontend.db.conn :as conn]
             [frontend.db.conn :as conn]
-            [frontend.db.default :as default-db]
+            [logseq.graph-parser.db.default :as default-db]
             [frontend.db.model]
             [frontend.db.model]
             [frontend.db.query-custom]
             [frontend.db.query-custom]
             [frontend.db.query-react]
             [frontend.db.query-react]
@@ -27,7 +27,6 @@
   get-short-repo-name
   get-short-repo-name
   datascript-db
   datascript-db
   get-db
   get-db
-  me-tx
   remove-conn!]
   remove-conn!]
 
 
  [frontend.db.utils
  [frontend.db.utils
@@ -39,28 +38,27 @@
 
 
  [frontend.db.model
  [frontend.db.model
   blocks-count blocks-count-cache clean-export! delete-blocks get-pre-block
   blocks-count blocks-count-cache clean-export! delete-blocks get-pre-block
-  delete-file! delete-file-blocks! delete-page-blocks delete-file-pages! delete-file-tx delete-files delete-pages-by-files
+  delete-file-blocks! delete-page-blocks delete-files delete-pages-by-files
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
   get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks
   get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks
   get-block-children-ids get-block-immediate-children get-block-page
   get-block-children-ids get-block-immediate-children get-block-page
-  get-blocks-contents get-custom-css
-  get-date-scheduled-or-deadlines get-db-type
-  get-file-blocks get-file-contents get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
-  get-file-pages get-files get-files-blocks get-files-full get-journals-length
-  get-latest-journals get-matched-blocks get-page get-page-alias get-page-alias-names get-paginated-blocks get-page-linked-refs-refed-pages
+  get-custom-css get-date-scheduled-or-deadlines
+  get-file-blocks get-file-last-modified-at get-file get-file-page get-file-page-id file-exists?
+  get-files get-files-blocks get-files-full get-journals-length
+  get-latest-journals get-page get-page-alias get-page-alias-names get-paginated-blocks get-page-linked-refs-refed-pages
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
   get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
   get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? page-alias-set pull-block
   journal-page? page-alias-set pull-block
-  set-file-last-modified-at! transact-files-db! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
+  set-file-last-modified-at! page-empty? page-exists? page-empty-or-dummy? get-alias-source-page
   set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition]
   set-file-content! has-children? get-namespace-pages get-all-namespace-relation get-pages-by-name-partition]
 
 
  [frontend.db.react
  [frontend.db.react
   get-current-page set-key-value
   get-current-page set-key-value
   remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   remove-key! remove-q! remove-query-component! add-q! add-query-component! clear-query-state!
   clear-query-state-without-refs-and-embeds! kv q
   clear-query-state-without-refs-and-embeds! kv q
-  query-state query-components query-entity-in-component remove-custom-query! set-new-result! sub-key-value refresh!]
+  query-state query-components remove-custom-query! set-new-result! sub-key-value refresh!]
 
 
  [frontend.db.query-custom
  [frontend.db.query-custom
   custom-query]
   custom-query]
@@ -68,7 +66,7 @@
  [frontend.db.query-react
  [frontend.db.query-react
   react-query custom-query-result-transform]
   react-query custom-query-result-transform]
 
 
- [frontend.db.default built-in-pages-names built-in-pages])
+ [logseq.graph-parser.db.default built-in-pages-names built-in-pages])
 
 
 (defn get-schema-version [db]
 (defn get-schema-version [db]
   (d/q
   (d/q
@@ -116,8 +114,8 @@
 
 
 ;; only save when user's idle
 ;; only save when user's idle
 
 
-;; TODO: pass as a parameter
-(defonce *sync-search-indice-f (atom nil))
+(def *db-listener (atom nil))
+
 (defn- repo-listen-to-tx!
 (defn- repo-listen-to-tx!
   [repo conn]
   [repo conn]
   (d/listen! conn :persistence
   (d/listen! conn :persistence
@@ -138,14 +136,8 @@
                      (state/set-last-transact-time! repo (util/time-ms))
                      (state/set-last-transact-time! repo (util/time-ms))
                      (persist-if-idle! repo)))
                      (persist-if-idle! repo)))
 
 
-                 ;; rebuild search indices
-                 (let [data (:tx-data tx-report)
-                       datoms (filter
-                               (fn [datom]
-                                 (contains? #{:block/name :block/content} (:a datom)))
-                               data)]
-                   (when-let [f @*sync-search-indice-f]
-                     (f datoms)))))))
+                 (when-let [db-listener @*db-listener]
+                   (db-listener repo tx-report))))))
 
 
 (defn listen-and-persist!
 (defn listen-and-persist!
   [repo]
   [repo]
@@ -153,16 +145,16 @@
     (repo-listen-to-tx! repo conn)))
     (repo-listen-to-tx! repo conn)))
 
 
 (defn start-db-conn!
 (defn start-db-conn!
-  ([me repo]
-   (start-db-conn! me repo {}))
-  ([me repo option]
-   (conn/start! me repo
+  ([repo]
+   (start-db-conn! repo {}))
+  ([repo option]
+   (conn/start! repo
                 (assoc option
                 (assoc option
                        :listen-handler listen-and-persist!))))
                        :listen-handler listen-and-persist!))))
 
 
 (defn restore-graph!
 (defn restore-graph!
   "Restore db from serialized db cache, and swap into the current db status"
   "Restore db from serialized db cache, and swap into the current db status"
-  [repo me]
+  [repo]
   (p/let [db-name (datascript-db repo)
   (p/let [db-name (datascript-db repo)
           db-conn (d/create-conn db-schema/schema)
           db-conn (d/create-conn db-schema/schema)
           _ (swap! conns assoc db-name db-conn)
           _ (swap! conns assoc db-name db-conn)
@@ -172,9 +164,8 @@
                                    (catch js/Error _e
                                    (catch js/Error _e
                                      (js/console.warn "Invalid graph cache")
                                      (js/console.warn "Invalid graph cache")
                                      (d/empty-db db-schema/schema)))
                                      (d/empty-db db-schema/schema)))
-                    attached-db (d/db-with stored-db (concat
-                                                      [(me-tx stored-db me)]
-                                                      default-db/built-in-pages)) ;; TODO bug overriding uuids?
+                    attached-db (d/db-with stored-db
+                                           default-db/built-in-pages) ;; TODO bug overriding uuids?
                     db (if (old-schema? attached-db)
                     db (if (old-schema? attached-db)
                          (db-migrate/migrate attached-db)
                          (db-migrate/migrate attached-db)
                          attached-db)]
                          attached-db)]
@@ -182,10 +173,10 @@
     (d/transact! db-conn [{:schema/version db-schema/version}])))
     (d/transact! db-conn [{:schema/version db-schema/version}])))
 
 
 (defn restore!
 (defn restore!
-  [{:keys [repos] :as me} _old-db-schema restore-config-handler]
+  [{:keys [repos]} _old-db-schema restore-config-handler]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]
   (let [repo (or (state/get-current-repo) (:url (first repos)))]
     (when repo
     (when repo
-      (p/let [_ (restore-graph! repo me)]
+      (p/let [_ (restore-graph! repo)]
         (restore-config-handler repo)
         (restore-config-handler repo)
         (listen-and-persist! repo)))))
         (listen-and-persist! repo)))))
 
 

+ 7 - 26
src/main/frontend/db/conn.cljs

@@ -1,15 +1,13 @@
 (ns frontend.db.conn
 (ns frontend.db.conn
   "Contains db connections."
   "Contains db connections."
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
-            [frontend.db-schema :as db-schema]
-            [frontend.db.default :as default-db]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.config :as config]
             [frontend.config :as config]
+            [frontend.util.text :as text-util]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
-            [logseq.graph-parser.util :as gp-util]
-            [datascript.core :as d]))
+            [logseq.graph-parser.db :as gp-db]))
 
 
 (defonce conns (atom {}))
 (defonce conns (atom {}))
 
 
@@ -24,7 +22,7 @@
   [repo]
   [repo]
   (cond
   (cond
     (mobile-util/native-platform?)
     (mobile-util/native-platform?)
-    (text/get-graph-name-from-path repo)
+    (text-util/get-graph-name-from-path repo)
 
 
     (config/local-db? repo)
     (config/local-db? repo)
     (config/get-local-dir repo)
     (config/get-local-dir repo)
@@ -68,30 +66,13 @@
   [repo]
   [repo]
   (swap! conns dissoc (datascript-db repo)))
   (swap! conns dissoc (datascript-db repo)))
 
 
-(defn me-tx
-  [_db {:keys [name email avatar]}]
-  (gp-util/remove-nils {:me/name name
-                     :me/email email
-                     :me/avatar avatar}))
-
 (defn start!
 (defn start!
-  ([me repo]
-   (start! me repo {}))
-  ([me repo {:keys [db-type listen-handler]}]
+  ([repo]
+   (start! repo {}))
+  ([repo {:keys [listen-handler]}]
    (let [db-name (datascript-db repo)
    (let [db-name (datascript-db repo)
-         db-conn (d/create-conn db-schema/schema)]
+         db-conn (gp-db/start-conn)]
      (swap! conns assoc db-name db-conn)
      (swap! conns assoc db-name db-conn)
-     (d/transact! db-conn [(cond-> {:schema/version db-schema/version}
-                             db-type
-                             (assoc :db/type db-type))
-                           {:block/name "card"
-                            :block/original-name "card"
-                            :block/uuid (d/squuid)}])
-     (when me
-       (d/transact! db-conn [(me-tx (d/db db-conn) me)]))
-
-     (d/transact! db-conn default-db/built-in-pages)
-
      (when listen-handler
      (when listen-handler
        (listen-handler repo)))))
        (listen-handler repo)))))
 
 

+ 5 - 88
src/main/frontend/db/model.cljs

@@ -8,7 +8,7 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
-            [frontend.db-schema :as db-schema]
+            [logseq.graph-parser.db.schema :as db-schema]
             [frontend.db.conn :as conn]
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
@@ -16,7 +16,7 @@
             [frontend.util :as util :refer [react]]
             [frontend.util :as util :refer [react]]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [frontend.db.rules :refer [rules]]
             [frontend.db.rules :refer [rules]]
-            [frontend.db.default :as default-db]
+            [logseq.graph-parser.db.default :as default-db]
             [frontend.util.drawer :as drawer]))
             [frontend.util.drawer :as drawer]))
 
 
 ;; lazy loading
 ;; lazy loading
@@ -56,18 +56,6 @@
     {:block/page [:db/id :block/name :block/original-name :block/journal-day]}
     {:block/page [:db/id :block/name :block/original-name :block/journal-day]}
     {:block/_parent ...}])
     {:block/_parent ...}])
 
 
-(defn transact-files-db!
-  ([tx-data]
-   (db-utils/transact! (state/get-current-repo) tx-data))
-  ([repo-url tx-data]
-   (when-not config/publishing?
-     (let [tx-data (->> (gp-util/remove-nils tx-data)
-                        (remove nil?)
-                        (map #(dissoc % :file/handle :file/type)))]
-       (when (seq tx-data)
-         (when-let [conn (conn/get-db repo-url false)]
-           (d/transact! conn (vec tx-data))))))))
-
 (defn pull-block
 (defn pull-block
   [id]
   [id]
   (let [repo (state/get-current-repo)]
   (let [repo (state/get-current-repo)]
@@ -208,16 +196,6 @@
            (conn/get-db repo-url) path)
            (conn/get-db repo-url) path)
       db-utils/seq-flatten))
       db-utils/seq-flatten))
 
 
-(defn get-file-pages
-  [repo-url path]
-  (-> (d/q '[:find ?page
-             :in $ ?path
-             :where
-             [?file :file/path ?path]
-             [?page :block/file ?file]]
-           (conn/get-db repo-url) path)
-      db-utils/seq-flatten))
-
 (defn set-file-last-modified-at!
 (defn set-file-last-modified-at!
   [repo path last-modified-at]
   [repo path last-modified-at]
   (when (and repo path last-modified-at)
   (when (and repo path last-modified-at)
@@ -240,19 +218,6 @@
     (when-let [db (conn/get-db repo)]
     (when-let [db (conn/get-db repo)]
       (d/entity db [:file/path path]))))
       (d/entity db [:file/path path]))))
 
 
-(defn get-file-contents
-  [repo]
-  (when-let [db (conn/get-db repo)]
-    (->>
-     (d/q
-      '[:find ?path ?content
-        :where
-        [?file :file/path ?path]
-        [?file :file/content ?content]]
-       db)
-     (into {}))))
-
-
 (defn get-files-full
 (defn get-files-full
   [repo]
   [repo]
   (when-let [db (conn/get-db repo)]
   (when-let [db (conn/get-db repo)]
@@ -666,7 +631,9 @@
         cached-ids-set (set (conj cached-ids page-id))
         cached-ids-set (set (conj cached-ids page-id))
         first-changed-id (cond
         first-changed-id (cond
                            (= (:real-outliner-op tx-meta) :indent-outdent)
                            (= (:real-outliner-op tx-meta) :indent-outdent)
-                           (last (:move-blocks tx-meta))
+                           (if (state/logical-outdenting?)
+                             (first (:move-blocks tx-meta))
+                             (last (:move-blocks tx-meta)))
 
 
                            (= outliner-op :move-blocks)
                            (= outliner-op :move-blocks)
                            (let [{:keys [move-blocks target from-page to-page]} tx-meta]
                            (let [{:keys [move-blocks target from-page to-page]} tx-meta]
@@ -1303,43 +1270,11 @@
     (get-block-referenced-blocks-ids id)
     (get-block-referenced-blocks-ids id)
     (get-page-referenced-blocks-ids page-name-or-block-uuid)))
     (get-page-referenced-blocks-ids page-name-or-block-uuid)))
 
 
-(defn get-matched-blocks
-  [match-fn limit]
-  (when-let [repo (state/get-current-repo)]
-    (let [pred (fn [_db content]
-                 (match-fn content))]
-      (->> (d/q
-            '[:find ?block
-              :in $ ?pred
-              :where
-              [?block :block/content ?content]
-              [(?pred $ ?content)]]
-            (conn/get-db)
-            pred)
-           (take limit)
-           db-utils/seq-flatten
-           (db-utils/pull-many '[:block/uuid
-                                 :block/content
-                                 :block/properties
-                                 :block/format
-                                 {:block/page [:block/name]}])))))
-
-;; TODO: Does the result preserves the order of the arguments?
-(defn get-blocks-contents
-  [repo block-uuids]
-  (let [db (conn/get-db repo)]
-    (db-utils/pull-many repo '[:block/content]
-                        (mapv (fn [id] [:block/uuid id]) block-uuids))))
-
 (defn journal-page?
 (defn journal-page?
   "sanitized page-name only"
   "sanitized page-name only"
   [page-name]
   [page-name]
   (:block/journal? (db-utils/entity [:block/name page-name])))
   (:block/journal? (db-utils/entity [:block/name page-name])))
 
 
-(defn get-db-type
-  [repo]
-  (db-utils/get-key-value repo :db/type))
-
 (defn get-public-pages
 (defn get-public-pages
   [db]
   [db]
   (-> (d/q
   (-> (d/q
@@ -1514,24 +1449,6 @@
                 block-eids (mapv :e datoms)]
                 block-eids (mapv :e datoms)]
             (mapv (fn [eid] [:db.fn/retractEntity eid]) block-eids)))))))
             (mapv (fn [eid] [:db.fn/retractEntity eid]) block-eids)))))))
 
 
-(defn delete-file-pages!
-  [repo-url path]
-  (let [pages (get-file-pages repo-url path)]
-    (mapv (fn [eid] [:db.fn/retractEntity eid]) pages)))
-
-(defn delete-file-tx
-  [repo-url file-path]
-  (->>
-   (concat
-    (delete-file-blocks! repo-url file-path)
-    (delete-file-pages! repo-url file-path)
-    [[:db.fn/retractEntity [:file/path file-path]]])
-   (remove nil?)))
-
-(defn delete-file!
-  [repo-url file-path]
-  (db-utils/transact! repo-url (delete-file-tx repo-url file-path)))
-
 (defn delete-pages-by-files
 (defn delete-pages-by-files
   [files]
   [files]
   (let [pages (->> (mapv get-file-page files)
   (let [pages (->> (mapv get-file-page files)

+ 13 - 11
src/main/frontend/db/query_dsl.cljs

@@ -14,6 +14,7 @@
             [frontend.db.rules :as rules]
             [frontend.db.rules :as rules]
             [frontend.template :as template]
             [frontend.template :as template]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
+            [frontend.util.text :as text-util]
             [frontend.util :as util]))
             [frontend.util :as util]))
 
 
 
 
@@ -443,17 +444,18 @@ Some bindings in this fn:
   [s]
   [s]
   (some-> s
   (some-> s
           (string/replace text/page-ref-re "\"[[$1]]\"")
           (string/replace text/page-ref-re "\"[[$1]]\"")
-          (string/replace text/between-re (fn [[_ x]]
-                                            (->> (string/split x #" ")
-                                                 (remove string/blank?)
-                                                 (map (fn [x]
-                                                        (if (or (contains? #{"+" "-"} (first x))
-                                                                (and (util/safe-re-find #"\d" (first x))
-                                                                     (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
-                                                          (keyword (name x))
-                                                          x)))
-                                                 (string/join " ")
-                                                 (util/format "(between %s)"))))))
+          (string/replace text-util/between-re
+                          (fn [[_ x]]
+                            (->> (string/split x #" ")
+                                 (remove string/blank?)
+                                 (map (fn [x]
+                                        (if (or (contains? #{"+" "-"} (first x))
+                                                (and (util/safe-re-find #"\d" (first x))
+                                                     (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
+                                          (keyword (name x))
+                                          x)))
+                                 (string/join " ")
+                                 (util/format "(between %s)"))))))
 
 
 (defn- add-bindings!
 (defn- add-bindings!
   [form q]
   [form q]

+ 5 - 3
src/main/frontend/db/query_react.cljs

@@ -76,8 +76,10 @@
                               remove-nested-children-blocks
                               remove-nested-children-blocks
                               (model/sort-by-left-recursive)
                               (model/sort-by-left-recursive)
                               (model/with-pages)))
                               (model/with-pages)))
-                   result)]
-      (if-let [result-transform (:result-transform q)]
+                   result)
+          result-transform-fn (:result-transform q)
+          repo (state/get-current-repo)]
+      (if-let [result-transform (if (keyword? result-transform-fn) (state/sub [:config repo :query/result-transforms result-transform-fn]) result-transform-fn)]
         (if-let [f (sci/eval-string (pr-str result-transform))]
         (if-let [f (sci/eval-string (pr-str result-transform))]
           (try
           (try
             (sci/call-fn f result)
             (sci/call-fn f result)
@@ -130,4 +132,4 @@
           k [:custom query']]
           k [:custom query']]
       (pprint "inputs (post-resolution):" resolved-inputs)
       (pprint "inputs (post-resolution):" resolved-inputs)
       (pprint "query-opts:" query-opts)
       (pprint "query-opts:" query-opts)
-      (apply react/q repo k query-opts query inputs))))
+      (apply react/q repo k query-opts query inputs))))

+ 0 - 15
src/main/frontend/db/react.cljs

@@ -137,21 +137,6 @@
 
 
 ;; Reactive query
 ;; Reactive query
 
 
-
-(defn query-entity-in-component
-  ([id-or-lookup-ref]
-   (db-utils/entity (state/get-current-repo) id-or-lookup-ref))
-  ([repo id-or-lookup-ref]
-   (let [k [:entity id-or-lookup-ref]
-         result-atom (:result (get @query-state k))]
-     (when-let [component *query-component*]
-       (add-query-component! k component))
-     (when-let [db (conn/get-db repo)]
-       (let [result (d/entity db id-or-lookup-ref)
-             result-atom (or result-atom (atom nil))]
-         (set! (.-state result-atom) result)
-         (add-q! k nil nil result-atom identity identity identity))))))
-
 (defn get-query-cached-result
 (defn get-query-cached-result
   [k]
   [k]
   (:result (get @query-state k)))
   (:result (get @query-state k)))

+ 363 - 20
src/main/frontend/dicts.cljc

@@ -147,8 +147,10 @@
         :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/edit-config-edn "Edit config.edn"
         :settings-page/edit-config-edn "Edit config.edn"
         :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/edit-custom-css "Edit custom.css"
+        :settings-page/edit-export-css "Edit export.css"
         :settings-page/custom-configuration "Custom configuration"
         :settings-page/custom-configuration "Custom configuration"
         :settings-page/custom-theme "Custom theme"
         :settings-page/custom-theme "Custom theme"
+        :settings-page/export-theme "Export theme"
         :settings-page/show-brackets "Show brackets"
         :settings-page/show-brackets "Show brackets"
         :settings-page/spell-checker "Spell checker"
         :settings-page/spell-checker "Spell checker"
         :settings-page/auto-updater "Auto updater"
         :settings-page/auto-updater "Auto updater"
@@ -1563,14 +1565,18 @@
         :file-sync/other-user-graph "El gráfico local actual está unido al gráfico remoto de otro usuario. Así que no se puede empezar a sincronizar"
         :file-sync/other-user-graph "El gráfico local actual está unido al gráfico remoto de otro usuario. Así que no se puede empezar a sincronizar"
         :file-sync/graph-deleted "El gráfico remoto actual se ha eliminado"}
         :file-sync/graph-deleted "El gráfico remoto actual se ha eliminado"}
 
 
-   :nb-NO {:on-boarding/demo-graph "This is a demo graph, changes will not be saved until you open a local folder."
-           :on-boarding/add-graph "Add a graph"
-           :on-boarding/open-local-dir "Open a local directory"
-           :on-boarding/new-graph-desc-1 "Logseq supports both Markdown and Org-mode. You can open an existing directory or create a new one on your device, a directory is also known simply as a folder. Your data will be stored only on this device."
-           :on-boarding/new-graph-desc-2 "After you have opened your directory, it will create three folders in that directory:"
-           :on-boarding/new-graph-desc-3 "/journals - store your journal pages"
-           :on-boarding/new-graph-desc-4 "/pages - store the other pages"
-           :on-boarding/new-graph-desc-5 "/logseq - store configuration, custom.css, and some metadata."
+   :nb-NO {:tutorial/text #?(:cljs (rc/inline "tutorial-no.md")
+                                :default "tutorial-no.md")
+           :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-no.md")
+                                       :default "dummy-notes-no.md")
+           :on-boarding/demo-graph "Dette er en demo graf, endringer vil ikke bli lagret før du åpner en lokal mappe."
+           :on-boarding/add-graph "Legg til en graf"
+           :on-boarding/open-local-dir "Åpne en lokal mappe"
+           :on-boarding/new-graph-desc-1 "Logseq støtter både Markdown og Org-mode. Du kan åpne en eksisterende mappe eller lage en ny på din enhet. Dine data vil bare bli lagret på denne enheten."
+           :on-boarding/new-graph-desc-2 "Etter at du har åpnet en mappe vil det blir opprettet tre undermapper i denne:"
+           :on-boarding/new-graph-desc-3 "/journals - lagrer dine dagboknotater"
+           :on-boarding/new-graph-desc-4 "/pages - lagrer andre sider"
+           :on-boarding/new-graph-desc-5 "/logseq - lagrer konfigurasjon, custom.css og noe metadata."
            :help/start "Kom i gang"
            :help/start "Kom i gang"
            :help/about "Om Logseq"
            :help/about "Om Logseq"
            :help/roadmap "Veikart"
            :help/roadmap "Veikart"
@@ -1592,7 +1598,7 @@
            :help/block-reference "Blokkreferanse"
            :help/block-reference "Blokkreferanse"
            :help/key-commands "Nøkkelkommandoer"
            :help/key-commands "Nøkkelkommandoer"
            :help/working-with-lists " (arbeide med lister)"
            :help/working-with-lists " (arbeide med lister)"
-           :help/select-nfs-browser " Vennligst bruk en annen nettleser (f.eks. siste chrome) som støtter NFS for å åpne en lokal mappe."
+           :help/select-nfs-browser " Vennligst bruk en annen nettleser (f.eks. siste Chrome) som støtter NFS for å åpne en lokal mappe."
            :undo "Angre"
            :undo "Angre"
            :redo "Gjør om"
            :redo "Gjør om"
            :general "Generell"
            :general "Generell"
@@ -1673,7 +1679,7 @@
            :draw/invalid-file "Kunne ikke laste inn den ugyldige excalidraw-filen"
            :draw/invalid-file "Kunne ikke laste inn den ugyldige excalidraw-filen"
            :draw/specify-title "Vennligst spesifiser en tittel først!"
            :draw/specify-title "Vennligst spesifiser en tittel først!"
            :draw/rename-success "Filen ble omdøpt!"
            :draw/rename-success "Filen ble omdøpt!"
-           :draw/rename-failure "Omdøpring av fil feilet, årsak: "
+           :draw/rename-failure "Omdøping av fil feilet, årsak: "
            :draw/title-placeholder "Uten navn"
            :draw/title-placeholder "Uten navn"
            :draw/save "Lagre"
            :draw/save "Lagre"
            :draw/save-changes "Lagre endringer"
            :draw/save-changes "Lagre endringer"
@@ -1694,10 +1700,10 @@
            :content/open-in-sidebar "Åpne i sidefeltet"
            :content/open-in-sidebar "Åpne i sidefeltet"
            :content/copy-as-json "Kopier som JSON"
            :content/copy-as-json "Kopier som JSON"
            :content/click-to-edit "Klikk for å redigere"
            :content/click-to-edit "Klikk for å redigere"
-           :settings-page/git-desc "is used for pages version control, you can click the vertical three dots menu to check the page's history."
-           :settings-page/git-confirm "You need to restart the app after updating the Git settings."
-           :settings-page/git-switcher-label "Enable Git auto commit"
-           :settings-page/git-commit-delay "Git auto commit seconds"
+           :settings-page/git-desc "brukes for versjonskontroll av sider. Du kan klikke på ...-menyen for å sjekke sidens historikk."
+           :settings-page/git-confirm "Du må starte appen på nytt etter å ha oppdatert Git innstillingene."
+           :settings-page/git-switcher-label "Skru på Git auto commit"
+           :settings-page/git-commit-delay "Git auto commit sekunder"
            :settings-page/edit-config-edn "Rediger config.edn for nåværende repo"
            :settings-page/edit-config-edn "Rediger config.edn for nåværende repo"
            :settings-page/edit-custom-css "Rediger custom.css"
            :settings-page/edit-custom-css "Rediger custom.css"
            :settings-page/custom-configuration "Tilpasset konfigurasjon"
            :settings-page/custom-configuration "Tilpasset konfigurasjon"
@@ -1828,7 +1834,7 @@
            :plugin/title "Tittel"
            :plugin/title "Tittel"
            :plugin/all "Alle"
            :plugin/all "Alle"
            :plugin/unpacked "Utpakket"
            :plugin/unpacked "Utpakket"
-           :plugin/delete-alert "Vil du avinstallere plugin [{1}]?"
+           :plugin/delete-alert "Vil du avinstallere utvidelse [{1}]?"
            :plugin/open-settings "Åpne innstillinger"
            :plugin/open-settings "Åpne innstillinger"
            :plugin/open-package "Åpne pakke"
            :plugin/open-package "Åpne pakke"
            :plugin/load-unpacked "Last inn utpakket utvidelse"
            :plugin/load-unpacked "Last inn utpakket utvidelse"
@@ -2173,7 +2179,13 @@
            :settings-page/network-proxy "Proxy de Rede"
            :settings-page/network-proxy "Proxy de Rede"
 
 
            :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
            :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
-           :file-sync/graph-deleted "O gráfico remoto atual foi excluído"}
+           :file-sync/graph-deleted "O gráfico remoto atual foi excluído"
+           
+           :page/copy-page-url "Copiar URL da página"
+           :page/file-sync-versions "Versões da página"
+           :plugin/not-installed "Não instalado"
+           :tutorial/dummy-notes "dummy-notes-en.md"
+           :tutorial/text "tutorial-en.md"}
 
 
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
            :on-boarding/add-graph "Adicionar grafo"
            :on-boarding/add-graph "Adicionar grafo"
@@ -3392,7 +3404,8 @@
         :select.graph/add-graph "Sì, aggiungi un nuovo grafo"
         :select.graph/add-graph "Sì, aggiungi un nuovo grafo"
 
 
         :file-sync/other-user-graph "Il grafo locale attuale è associato al grafo remoto di un altro utente. Non è quindi possibile avviare la sincronizzazione."
         :file-sync/other-user-graph "Il grafo locale attuale è associato al grafo remoto di un altro utente. Non è quindi possibile avviare la sincronizzazione."
-        :file-sync/graph-deleted "Il grafo attuale è stato eliminato"}
+        :file-sync/graph-deleted "Il grafo attuale è stato eliminato"
+        :settings-page/enable-encryption "Crittografia"}
 
 
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
                                 :default "tutorial-tr.md")
                                 :default "tutorial-tr.md")
@@ -3538,8 +3551,10 @@
         :settings-page/git-commit-delay "Git otomatik commit saniyesi"
         :settings-page/git-commit-delay "Git otomatik commit saniyesi"
         :settings-page/edit-config-edn "config.edn dosyasını düzenle"
         :settings-page/edit-config-edn "config.edn dosyasını düzenle"
         :settings-page/edit-custom-css "custom.css dosyasını düzenle"
         :settings-page/edit-custom-css "custom.css dosyasını düzenle"
+        :settings-page/edit-export-css "export.css dosyasını düzenle"
         :settings-page/custom-configuration "Özel yapılandırma"
         :settings-page/custom-configuration "Özel yapılandırma"
         :settings-page/custom-theme "Özel tema"
         :settings-page/custom-theme "Özel tema"
+        :settings-page/export-theme "Temayı dışarı aktar"
         :settings-page/show-brackets "Köşeli ayraçları göster"
         :settings-page/show-brackets "Köşeli ayraçları göster"
         :settings-page/spell-checker "Yazım denetleyici"
         :settings-page/spell-checker "Yazım denetleyici"
         :settings-page/auto-updater "Otomatik güncelleme"
         :settings-page/auto-updater "Otomatik güncelleme"
@@ -3717,8 +3732,335 @@
 
 
         :file-sync/other-user-graph "Geçerli yerel grafik, diğer kullanıcının uzak grafiğine bağlıdır. Bu yüzden senkronizasyon başlatılamıyor."
         :file-sync/other-user-graph "Geçerli yerel grafik, diğer kullanıcının uzak grafiğine bağlıdır. Bu yüzden senkronizasyon başlatılamıyor."
         :file-sync/graph-deleted "Geçerli uzak grafik silindi"}
         :file-sync/graph-deleted "Geçerli uzak grafik silindi"}
-
-   :tongue/fallback :en})
+   
+   :ko {:tutorial/text #?(:cljs (rc/inline "tutorial-ko.md")
+                          :default "tutorial-ko.md")
+        :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-ko.md")
+                                 :default "dummy-notes-ko.md")
+        :on-boarding/demo-graph "이 그래프는 데모 그래프입니다. 로컬 폴더를 열기 전까지 변경 사항은 저장되지 않습니다."
+        :on-boarding/add-graph "그래프 추가"
+        :on-boarding/open-local-dir "로컬 폴더 열기"
+        :on-boarding/new-graph-desc-1 "Logseq은 Markdown과 Org-Mode를 모두 지원합니다. 기기에서 존재하는 디렉토리(폴더)를 열거나 새 디렉토리를 만들 수 있습니다. 사용자의 데이터는 오직 이 기기에만 저장됩니다."
+        :on-boarding/new-graph-desc-2 "디렉토리를 열면 Logseq은 3가지 폴더를 만들 것입니다:"
+        :on-boarding/new-graph-desc-3 "/journals - 일지 페이지들을 저장합니다."
+        :on-boarding/new-graph-desc-4 "/pages - 기타 페이지들을 저장합니다."
+        :on-boarding/new-graph-desc-5 "/logseq - 사용자 설정과 custom.css, 그리고 몇 가지 메타데이터를 저장합니다."
+        :help/start "시작"
+        :help/about "Logseq에 대하여"
+        :help/roadmap "로드맵"
+        :help/bug "오류 제보"
+        :help/feature "기능 제안"
+        :help/changelog "변경 사항"
+        :help/blog "Logseq 블로그"
+        :help/docs "안내 문서"
+        :help/privacy "개인정보 보호정책"
+        :help/terms "약관"
+        :help/community "디스코드 커뮤니티"
+        :help/awesome-logseq "Awesome Logseq"
+        :help/shortcuts "키보드 단축키"
+        :help/shortcuts-triggers "트리거"
+        :help/shortcut "단축키"
+        :help/slash-autocomplete "Slash 자동 완성"
+        :help/block-content-autocomplete "블록 콘텐츠 자동 완성"
+        :help/reference-autocomplete "페이지 레퍼런스 자동 완성"
+        :help/block-reference "블록 레퍼런스"
+        :help/key-commands "키 커맨드"
+        :help/working-with-lists " (목록을 사용하는 법)"
+        :help/select-nfs-browser "최신 Chrome과 같이, NFS 기능을 지원하여 로컬 디렉토리를 열 수 있는 기타 브라우저에서 사용할 수 있습니다."
+        :undo "실행 취소"
+        :redo "다시 실행"
+        :general "일반"
+        :more "더 보기"
+        :search/result-for "검색 결과: "
+        :search/items "항목"
+        :search/page-names "페이지 이름 검색"
+        :help/context-menu "블록 컨텍스트 메뉴"
+        :help/fold-unfold "블록 접기/열기 (편집 모드가 아닐 때)"
+        :help/markdown-syntax "Markdown 문법"
+        :help/org-mode-syntax "Org mode 문법"
+        :bold "볼드체"
+        :italics "이탤릭체"
+        :html-link "HTML 링크"
+        :highlight "하이라이트"
+        :strikethrough "취소선"
+        :code "코드"
+        :right-side-bar/help "도움말"
+        :right-side-bar/switch-theme "테마 모드"
+        :right-side-bar/theme "{1} 테마"
+        :right-side-bar/page "페이지 그래프"
+        :right-side-bar/recent "최근 페이지"
+        :right-side-bar/contents "콘텐츠"
+        :right-side-bar/favorites "즐겨찾기"
+        :right-side-bar/page-graph "페이지 그래프"
+        :right-side-bar/block-ref "블록 레퍼런스"
+        :right-side-bar/graph-view "그래프 뷰"
+        :right-side-bar/all-pages "모든 페이지"
+        :right-side-bar/flashcards "플래시 카드"
+        :right-side-bar/new-page "새 페이지"
+        :left-side-bar/journals "일지"
+        :left-side-bar/new-page "새 페이지"
+        :left-side-bar/nav-favorites "즐겨찾기"
+        :left-side-bar/nav-shortcuts "단축키"
+        :left-side-bar/nav-recent-pages "최근 페이지"
+        :format/preferred-mode "어떤 모드를 사용하시겠습니까?"
+        :format/markdown "Markdown"
+        :format/org-mode "Org mode"
+        :reference/linked "링크된 레퍼런스"
+        :reference/unlinked-ref "링크되지 않은 레퍼런스"
+        :page/presentation-mode "프레젠테이션 모드"
+        :page/edit-properties-placeholder "속성"
+        :page/delete-success "{1} 페이지가 성공적으로 삭제되었습니다."
+        :page/delete-confirmation "이 페이지와 페이지의 파일들을 삭제하시겠습니까?"
+        :page/rename-to "\"{1}\" 페이지의 이름을 무엇으로 바꾸시겠습니까?"
+        :page/priority "중요도 \"{1}\""
+        :page/copy-to-json "전체 페이지를 JSON으로 복사"
+        :page/rename "페이지 이름 바꾸기"
+        :page/open-in-finder "디렉토리에서 열기"
+        :page/open-with-default-app "기본 앱으로 열기"
+        :page/action-publish "출판"
+        :page/make-public "출판 전 공개 상태로 만들기"
+        :page/version-history "페이지 편집 기록 확인"
+        :page/open-backup-directory "페이지 백업 디렉토리 열기"
+        :page/file-sync-versions "페이지 버전"
+        :page/make-private "비공개 상태로 만들기"
+        :page/delete "페이지 삭제"
+        :page/add-to-favorites "즐겨찾기에 추가"
+        :page/unfavorite "즐겨찾기에서 삭제"
+        :page/show-journals "일지 보기"
+        :page/show-name "페이지 이름 보기"
+        :page/hide-name "페이지 이름 숨기기"
+        :block/name "페이지 일므"
+        :page/last-modified "마지막 편집 시간:"
+        :page/new-title "새 문서의 제목이 무엇입니까?"
+        :page/earlier "이전"
+        :page/no-more-journals "일지 더 없음"
+        :page/copy-page-url "페이지 URL 복사"
+        :journal/multiple-files-with-different-formats "같은 달에 여러 포맷의 일지 파일이 확인되었습니다. 각 달마다 하나의 일지 파일만 사용하시기 바랍니다."
+        :journal/go-to "파일로 가기"
+        :file/name "파일 이름"
+        :file/file "파일: "
+        :file/last-modified-at "마지막 편집 시간:"
+        :file/no-data "데이터 없음"
+        :file/format-not-supported "확장자 .{1} 형태의 파일은 지원되지 않습니다."
+        :page/created-at "생성 시간:"
+        :page/updated-at "수정 시간:"
+        :page/backlinks "역링크"
+        :editor/block-search "블록 검색"
+        :editor/image-uploading "업로드 중"
+        :draw/invalid-file "유효하지 않은 excalidraw 파일을 불러올 수 없습니다."
+        :draw/specify-title "제목을 먼저 입력하십시오."
+        :draw/rename-success "파일 이름이 성공적으로 변경되었습니다."
+        :draw/rename-failure "파일 이름 변경이 다음 이유로 실패하였습니다: "
+        :draw/title-placeholder "무제"
+        :draw/save "저장"
+        :draw/save-changes "변경사항 저장"
+        :draw/new-file "새 파일"
+        :draw/list-files "파일 목록"
+        :draw/delete "삭제"
+        :draw/more-options "더 보기"
+        :draw/back-to-logseq "Logseq으로 돌아가기"
+        :text/image "이미지"
+        :asset/confirm-delete "정말 {1} 을/를 삭제하겠습니까?"
+        :asset/physical-delete "파일 또한 삭제 (복구 불가능)"
+        :content/copy "복사"
+        :content/cut "잘라내기"
+        :content/make-todos "{1} 만들기"
+        :content/copy-block-ref "블록 주소 복사"
+        :content/copy-block-emebed "블록 임베드 복사"
+        :content/focus-on-block "블록 포커스"
+        :content/open-in-sidebar "사이드바에서 열기"
+        :content/copy-as-json "JSON으로 복사"
+        :content/click-to-edit "클릭하여 수정"
+        :settings-page/git-desc "페이지 버전 컨트롤을 위해 사용되며, 점 세개 메뉴를 클릭하여 페이지의 변경 기록을 확인할 수 있습니다."
+        :settings-page/git-confirm "Git 설정을 변경한 뒤 앱을 재시작해야 합니다."
+        :settings-page/git-switcher-label "Git 자동 커밋 설정"
+        :settings-page/git-commit-delay "Git 자동 커밋 간격 (초)"
+        :settings-page/edit-config-edn "config.edn 수정"
+        :settings-page/edit-custom-css "custom.css 수정"
+        :settings-page/edit-export-css "export.css 수정"
+        :settings-page/custom-configuration "사용자 설정"
+        :settings-page/custom-theme "사용자 테마"
+        :settings-page/export-theme "테마 내보내기"
+        :settings-page/show-brackets "대괄호 표시"
+        :settings-page/spell-checker "문법 검사"
+        :settings-page/auto-updater "자동 업데이트"
+        :settings-page/disable-sentry "사용 통계와 분석을 Logseq에 전송"
+        :settings-page/preferred-outdenting "논리적 아웃덴팅"
+        :settings-page/custom-date-format "선호하는 날짜 형식"
+        :settings-page/preferred-file-format "선호하는 파일 형식"
+        :settings-page/preferred-workflow "선호하는 워크플로우"
+        :settings-page/enable-shortcut-tooltip "단축키 툴팁 활성화"
+        :settings-page/enable-timetracking "시간 추적 활성화"
+        :settings-page/enable-tooltip "툴팁 활성화"
+        :settings-page/enable-journals "일지 활성화"
+        :settings-page/enable-all-pages-public "출판할 때 모든 페이지 공개로 설정"
+        :settings-page/enable-encryption "암호화"
+        :settings-page/customize-shortcuts "키보드 단축키"
+        :settings-page/shortcut-settings "단축키 설정"
+        :settings-page/home-default-page "기본 홈 페이지 설정"
+        :settings-page/enable-block-time "블록 타임스탬프"
+        :settings-page/clear-cache "캐시 지우기"
+        :settings-page/clear "지우기"
+        :settings-page/developer-mode "개발자 모드"
+        :settings-page/enable-developer-mode "개발자 모드 활성화"
+        :settings-page/disable-developer-mode "개발자 모드 비활성화"
+        :settings-page/developer-mode-desc "개발자 모드는 컨트리뷰터와 확장 프로그램 개발자들이 Logseq과의 연동을 더 쉽고 효율적으로 테스트할 수 있게 도와줍니다."
+        :settings-page/current-version "현재 버전"
+        :settings-page/current-graph "현재 그래프"
+        :settings-page/tab-general "일반"
+        :settings-page/tab-editor "에디터"
+        :settings-page/tab-shortcuts "단축키"
+        :settings-page/tab-version-control "버전 컨트롤"
+        :settings-page/tab-advanced "고급"
+        :settings-page/plugin-system "플러그인 시스템"
+        :settings-page/network-proxy "네트워크 프록시"
+        :logseq "Logseq"
+        :on "ON"
+        :more-options "더 많은 옵션"
+        :to "to"
+        :yes "예"
+        :no "아니오"
+        :submit "제출"
+        :cancel "취소"
+        :close "닫기"
+        :delete "삭제"
+        :save "저장"
+        :type "타입"
+        :host "호스트"
+        :port "포트"
+        :re-index "인덱스 재생성"
+        :re-index-detail "그래프 다시 빌드"
+        :re-index-multiple-windows-warning "그래프를 다시 인덱싱 전 다른 윈도우를 모두 닫아야 합니다."
+        :re-index-discard-unsaved-changes-warning "인덱싱을 다시 하게 되면 현재 나타나 있는 그래프가 사라지며, 하드 디스크에 저장된 파일대로 그래프를 재구성하게 됩니다. 저장되지 않은 변경사항들이 삭제되며 약간의 시간이 걸릴 수 있습니다. 계속하시겠습니까?"
+        :open-new-window "새 창"
+        :sync-from-local-files "새로고침"
+        :sync-from-local-files-detail "로컬 파일로부터 변경 사항 불러오기"
+        :sync-from-local-changes-detected "그래프를 새로 고치면 로컬 디스크에서 Logseq과는 다르게 변경된 파일들을 감지하고 처리합니다. 계속하시겠습니까?"
+
+        :unlink "링크 해제"
+        :search/publishing "검색"
+        :search "페이지를 검색하거나 생성"
+        :page-search "현재 페이지에서 검색"
+        :graph-search "그래프 검색"
+        :new-page "새 페이지"
+        :new-file "새 파일"
+        :new-graph "새 그래프"
+        :graph "그래프"
+        :graph-view "그래프 보기"
+        :graph/persist "Logseq가 내부 상태를 동기화 중입니다. 잠시만 기다려주십시오."
+        :graph/persist-error "내부 상태 동기화에 실패했습니다."
+        :graph/save "저장 중..."
+        :graph/save-success "저장 완료"
+        :graph/save-error "저장 실패"
+        :cards-view "카드 보기"
+        :publishing "출판"
+        :export "내보내기"
+        :export-graph "그래프 내보내기"
+        :export-page "페이지 내보내기"
+        :export-markdown "표준 Markdown으로 내보내기 (블록 속성 미포함)"
+        :export-opml "OPML으로 내보내기"
+        :export-public-pages "공개 페이지 내보내기"
+        :export-json "JSON으로 내보내기"
+        :export-roam-json "Roam JSON으로 내보내기"
+        :export-edn "EDN으로 내보내기"
+        :export-datascript-edn "Datascript EDN으로 내보내기"
+        :convert-markdown "Markdown 헤딩을 Unordered List들로 내보내기 (# -> -)"
+        :all-graphs "모든 그래프"
+        :all-pages "모든 페이지"
+        :all-files "모든 파일"
+        :remove-orphaned-pages "고립된 페이지 삭제"
+        :all-journals "모든 일지"
+        :my-publishing "나의 출판"
+        :settings "설정"
+        :settings-of-plugins "플러그인 설정"
+        :plugins "플러그인"
+        :themes "테마"
+        :developer-mode-alert "플러그인 시스템을 활성화하려면 앱을 재시작해야 합니다. 지금 재시작하시겠습니까?"
+        :relaunch-confirm-to-work "완료하려면 앱을 재시작해야 합니다. 지금 재시작하시겠습니까?"
+        :import "불러오기"
+        :join-community "커뮤니티에 참여"
+        :sponsor-us "후원"
+        :discord-title "디스코드"
+        :help-shortcut-title "다른 단축키와 도움말 확인하기"
+        :loading "로딩 중"
+        :cloning "복제 중"
+        :parsing-files "파일 파싱 중"
+        :loading-files "파일 로딩 중"
+        :login "로그인"
+        :logout "로그아웃"
+        :go-to "다음으로 이동: "
+        :or "또는"
+        :download "다운로드"
+        :repo/download-zip "ZIP 다운로드"
+        :language "언어"
+        :white "라이트 모드"
+        :dark "다크 모드"
+        :remove-background "배경 제거"
+        :open "열기"
+        :open-a-directory "로컬 디렉토리 열기"
+        :user/delete-account "계정 삭제"
+        :user/delete-your-account "사용자의 계정 삭제"
+        :user/delete-account-notice "logseq.com에 출판된 모든 문서가 삭제됩니다."
+
+        :help/shortcut-page-title "키보드 단축키"
+
+        :plugin/installed "설치됨"
+        :plugin/not-installed "설치되지 않음"
+        :plugin/installing "설치 중"
+        :plugin/install "설치"
+        :plugin/reload "다시 불러오기"
+        :plugin/update "업데이트"
+        :plugin/check-update "업데이트 확인"
+        :plugin/check-all-updates "모든 업데이트 확인"
+        :plugin/refresh-lists "리스트 새로고침"
+        :plugin/enabled "활성화됨"
+        :plugin/disabled "비활성화됨"
+        :plugin/update-available "업데이트 가능"
+        :plugin/updating "업데이트 중"
+        :plugin/uninstall "설지 제거"
+        :plugin/marketplace "마켓플레이스"
+        :plugin/downloads "다운로드"
+        :plugin/stars "스타"
+        :plugin/title "제목"
+        :plugin/all "전체"
+        :plugin/unpacked "압축 해제됨"
+        :plugin/delete-alert "다음 [{1}] 플러그인을 삭제하시겠습니까?"
+        :plugin/open-settings "설정 열기"
+        :plugin/open-package "패키지 열기"
+        :plugin/load-unpacked "압축 해제된 플러그인 불러오기"
+        :plugin/open-preferences "플러그인 설정 열기"
+        :plugin/restart "앱 다시 시작"
+        :plugin/unpacked-tips "플러그인 디렉토리 열기"
+        :plugin/contribute "✨ 새 플러그인을 만들고 기여하기"
+        :plugin/marketplace-tips "플러그인을 처음 설치하고 정상적으로 동작하지 않으면 Logseq를 다시 시작해 보십시오."
+        :plugin/up-to-date "최신 상태입니다."
+        :plugin/custom-js-alert "custom.js 파일을 감지했습니다. 실행을 허용하겠습니까? (파일의 내용을 이해하지 못한다면 보안과 안전 상의 이유로 실행하지 않는 것이 권장됩니다."
+
+        :pdf/copy-ref "레퍼런스 복사하기"
+        :pdf/copy-text "텍스트 복사하기"
+        :pdf/linked-ref "링크된 레퍼런스"
+        :pdf/toggle-dashed "영역 하이라이트를 위해 Dashed style 사용"
+
+        :updater/new-version-install "새 버전이 다운로드되었습니다."
+        :updater/quit-and-install "다시 시작하여 설치하십시오."
+
+        :paginates/pages "총 {1} 페이지"
+        :paginates/prev "이전"
+        :paginates/next "다음"
+
+        :tips/all-done "모두 완료"
+
+        :command-palette/prompt "커맨드를 입력하십시오"
+        :select/default-prompt "하나를 선택하십시오"
+        :select.graph/prompt "그래프를 선택하십시오."
+        :select.graph/empty-placeholder-description "일치하는 그래프가 없습니다. 새로 추가하시겠습니까?"
+        :select.graph/add-graph "네, 새 그래프를 추가합니다"
+
+        :file-sync/other-user-graph "현재 로컬 그래프가 다른 유저의 리모트 그래프와 충돌합니다. 동기화를 시작할 수 없습니다."
+        :file-sync/graph-deleted "현재 리모트 그래프를 삭제했습니다."
+        }
+     
+     :tongue/fallback :en})
 
 
 (def languages [{:label "English" :value :en}
 (def languages [{:label "English" :value :en}
                 {:label "Français" :value :fr}
                 {:label "Français" :value :fr}
@@ -3733,4 +4075,5 @@
                 {:label "Русский" :value :ru}
                 {:label "Русский" :value :ru}
                 {:label "日本語" :value :ja}
                 {:label "日本語" :value :ja}
                 {:label "Italiano" :value :it}
                 {:label "Italiano" :value :it}
-                {:label "Türkçe" :value :tr}])
+                {:label "Türkçe" :value :tr}
+                {:label "한국어" :value :ko}])

+ 2 - 2
src/main/frontend/diff.cljs

@@ -6,7 +6,7 @@
             [cljs-bean.core :as bean]
             [cljs-bean.core :as bean]
             [frontend.util :as util]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.text :as text]))
+            [frontend.util.text :as text-util]))
 
 
 (defn diff
 (defn diff
   [s1 s2]
   [s1 s2]
@@ -47,7 +47,7 @@
 
 
                       :else
                       :else
                       (recur r1 t2 (inc i1) i2))))
                       (recur r1 t2 (inc i1) i2))))
-            current-line (text/get-current-line-by-pos markup pos)]
+            current-line (text-util/get-current-line-by-pos markup pos)]
         (cond
         (cond
           (= (util/nth-safe markup pos)
           (= (util/nth-safe markup pos)
              (util/nth-safe markup (inc pos))
              (util/nth-safe markup (inc pos))

+ 23 - 19
src/main/frontend/extensions/calc.cljc

@@ -3,6 +3,9 @@
   (:require [clojure.edn :as edn]
   (:require [clojure.edn :as edn]
             [clojure.string :as str]
             [clojure.string :as str]
             [frontend.util :as util]
             [frontend.util :as util]
+
+            [bignumber.js :as bn]
+
             #?(:clj [clojure.java.io :as io])
             #?(:clj [clojure.java.io :as io])
             #?(:cljs [shadow.resource :as rc])
             #?(:cljs [shadow.resource :as rc])
             #?(:cljs [rum.core :as rum])
             #?(:cljs [rum.core :as rum])
@@ -24,35 +27,36 @@
 
 
 (defn new-env [] (atom {}))
 (defn new-env [] (atom {}))
 
 
+;; TODO: Set DECIMAL_PLACES https://mikemcl.github.io/bignumber.js/#decimal-places
+
 (defn eval* [env ast]
 (defn eval* [env ast]
   (insta/transform
   (insta/transform
-   {:number     (comp edn/read-string #(str/replace % "," ""))
-    :percent    (fn percent [a] (/ a 100.00))
-    :scientific edn/read-string
-    :negterm    (fn neg [a] (- a))
+   {:number     (comp bn/BigNumber #(str/replace % "," ""))
+    :percent    (fn percent [a] (-> a (.dividedBy 100.00)))
+    :scientific (comp bn/BigNumber edn/read-string)
+    :negterm    (fn neg [a] (-> a (.negated)))
     :expr       identity
     :expr       identity
-    :add        +
-    :sub        -
-    :mul        *
-    :div        /
-    :pow        (fn pow [a b]
-                  #?(:clj (java.lang.Math/pow a b) :cljs (js/Math.pow a b)))
+    :add        (fn add [a b] (-> a (.plus b)))
+    :sub        (fn sub [a b] (-> a (.minus b)))
+    :mul        (fn mul [a b] (-> a (.multipliedBy b)))
+    :div        (fn div [a b] (-> a (.dividedBy b)))
+    :pow        (fn pow [a b] (-> a (.exponentiatedBy b)))
     :log        (fn log [a]
     :log        (fn log [a]
-                  #?(:clj (java.lang.Math/log10 a) :cljs (js/Math.log10 a)))
+                  #?(:clj (java.lang.Math/log10 a) :cljs (bn/BigNumber (js/Math.log10 a))))
     :ln         (fn ln [a]
     :ln         (fn ln [a]
-                  #?(:clj (java.lang.Math/log a) :cljs (js/Math.log a)))
+                  #?(:clj (java.lang.Math/log a) :cljs (bn/BigNumber (js/Math.log a))))
     :sin        (fn sin [a]
     :sin        (fn sin [a]
-                  #?(:clj (java.lang.Math/sin a) :cljs (js/Math.sin a)))
+                  #?(:clj (java.lang.Math/sin a) :cljs (bn/BigNumber(js/Math.sin a))))
     :cos        (fn cos [a]
     :cos        (fn cos [a]
-                  #?(:clj (java.lang.Math/cos a) :cljs (js/Math.cos a)))
+                  #?(:clj (java.lang.Math/cos a) :cljs (bn/BigNumber(js/Math.cos a))))
     :tan        (fn tan [a]
     :tan        (fn tan [a]
-                  #?(:clj (java.lang.Math/tan a) :cljs (js/Math.tan a)))
+                  #?(:clj (java.lang.Math/tan a) :cljs (bn/BigNumber(js/Math.tan a))))
     :atan       (fn atan [a]
     :atan       (fn atan [a]
-                  #?(:clj (java.lang.Math/atan a) :cljs (js/Math.atan a)))
+                  #?(:clj (java.lang.Math/atan a) :cljs (bn/BigNumber(js/Math.atan a))))
     :asin       (fn asin [a]
     :asin       (fn asin [a]
-                  #?(:clj (java.lang.Math/asin a) :cljs (js/Math.asin a)))
+                  #?(:clj (java.lang.Math/asin a) :cljs (bn/BigNumber(js/Math.asin a))))
     :acos       (fn acos [a]
     :acos       (fn acos [a]
-                  #?(:clj (java.lang.Math/acos a) :cljs (js/Math.acos a)))
+                  #?(:clj (java.lang.Math/acos a) :cljs (bn/BigNumber(js/Math.acos a))))
     :assignment (fn assign! [var val]
     :assignment (fn assign! [var val]
                   (swap! env assoc var val)
                   (swap! env assoc var val)
                   val)
                   val)
@@ -101,4 +105,4 @@
            [:span (cond
            [:span (cond
                     (nil? line)           ""
                     (nil? line)           ""
                     (failure? line) "?"
                     (failure? line) "?"
-                    :else                 line)]])])))
+                    :else                 (str line))]])])))

+ 3 - 0
src/main/frontend/extensions/code.cljs

@@ -265,6 +265,9 @@
                              (state/clear-selection!)
                              (state/clear-selection!)
                              (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
                              (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
                                (state/set-editing! id (.getValue editor) block nil false))))
                                (state/set-editing! id (.getValue editor) block nil false))))
+        (.addEventListener element "touchstart"
+                           (fn [e]
+                             (.stopPropagation e)))
         (.save editor)
         (.save editor)
         (.refresh editor)
         (.refresh editor)
         (when default-open?
         (when default-open?

+ 179 - 124
src/main/frontend/extensions/html_parser.cljs

@@ -20,152 +20,207 @@
 
 
                (str (hiccup-without-style hiccup))))
                (str (hiccup-without-style hiccup))))
 
 
+(def allowed-tags
+  #{:address, :article, :aside, :footer, :header,
+    :h1, :h2, :h3, :h4, :h5, :h6, :hgroup,
+    :main, :nav, :section,
+    :blockquote, :dd, :div, :dl, :dt, :figcaption, :figure,
+    :hr, :li, :ol, :p, :pre, :ul,
+    :a, :abbr, :b, :bdi, :bdo, :br, :cite, :code, :data, :dfn,
+    :em, :i, :kbd, :mark, :q,
+    :rb, :rp, :rt, :rtc, :ruby,
+    :s, :samp, :small, :span, :strong, :sub, :sup, :time, :u, :var, :wbr,
+    :caption, :col, :colgroup, :table, :tbody, :td, :tfoot, :th,
+    :thead, :tr
+    :body :html})
+
 (defn ^:large-vars/cleanup-todo hiccup->doc-inner
 (defn ^:large-vars/cleanup-todo hiccup->doc-inner
-  [format hiccup]
-  (let [transform-fn (fn [hiccup]
-                       (hiccup->doc-inner format hiccup))
-        block-pattern (config/get-block-pattern format)
-        map-join (fn [children] (apply str (map transform-fn children)))
+  [format hiccup opts]
+  (let [transform-fn (fn [hiccup opts]
+                       (hiccup->doc-inner format hiccup opts))
+        block-pattern (if (= format :markdown)
+                        "#"
+                        (config/get-block-pattern format))
+        map-join (fn [children] (apply str (map #(transform-fn % opts) children)))
         block-transform (fn [level children]
         block-transform (fn [level children]
                           (str (apply str (repeat level block-pattern))
                           (str (apply str (repeat level block-pattern))
                                " "
                                " "
-                               (->> (map transform-fn children)
+                               (->> (map #(transform-fn % opts) children)
                                     (string/join " "))
                                     (string/join " "))
                                "\n"))
                                "\n"))
-        emphasis-transform (fn [tag _attrs children]
-                             (let [pattern (cond
+        emphasis-transform (fn [tag attrs children]
+                             (let [style (:style attrs)
+                                   [bold? italic? underline? strike-through? mark?]
+                                   (when style
+                                     [(or (string/includes? style "font-weight:700")
+                                          (string/includes? style "font-weight:600"))
+                                      (string/includes? style "font-style:italic")
+                                      (string/includes? style "text-decoration:underline")
+                                      (string/includes? style "text-decoration:line-through")
+                                      (string/includes? style "background-color:#")])
+                                   pattern (cond
                                              (contains? #{:b :strong} tag)
                                              (contains? #{:b :strong} tag)
-                                             (config/get-bold format)
+                                             (when-not (and style (string/includes? style "font-weight:normal"))
+                                               (config/get-bold format))
                                              (contains? #{:i :em} tag)
                                              (contains? #{:i :em} tag)
-                                             (config/get-italic format)
+                                             (when-not (and style (string/includes? style "font-style:normal"))
+                                               (config/get-italic format))
                                              (contains? #{:ins} tag)
                                              (contains? #{:ins} tag)
-                                             (config/get-underline format)
+                                             (when-not (and style (string/includes? style "text-decoration:normal"))
+                                               (config/get-underline format))
                                              (contains? #{:del} tag)
                                              (contains? #{:del} tag)
-                                             (config/get-strike-through format)
+                                             (when-not (and style (string/includes? style "text-decoration:normal"))
+                                               (config/get-strike-through format))
                                              (contains? #{:mark} tag)
                                              (contains? #{:mark} tag)
-                                             (config/get-highlight format)
+                                             (when-not (and style (string/includes? style "background-color:transparent"))
+                                               (config/get-highlight format))
+                                             (and (contains? #{:span} tag)
+                                                  (not (every? string/blank? children)))
+                                             (remove nil?
+                                                     [(when bold? (config/get-bold format))
+                                                      (when italic? (config/get-italic format))
+                                                      (when underline? (config/get-underline format))
+                                                      (when strike-through? (config/get-strike-through format))
+                                                      (when mark? (config/get-highlight format))])
                                              :else
                                              :else
-                                             nil)]
-                               (str pattern (map-join children) pattern)))
+                                             nil)
+                                   children' (map-join children)]
+                               (when-not (string/blank? children')
+                                 (str (if (string? pattern) pattern (apply str pattern))
+                                      children'
+                                      (if (string? pattern) pattern (apply str (reverse pattern)))))))
         wrapper (fn [tag content]
         wrapper (fn [tag content]
-                  (cond
-                    (contains? #{:p :hr :ul :ol :dl :table :pre :blockquote :aside :canvas
-                                 :center :figure :figcaption :fieldset :div :footer
-                                 :header} tag)
-                    (str "\n\n" content "\n\n")
+                  (let [content (cond
+                                  (not (contains? allowed-tags tag))
+                                  nil
+
+                                  (contains? #{:comment :head :style :xml} tag)
+                                  nil
+
+                                  (and (= tag :p) (:in-table? opts))
+                                  content
 
 
-                    (contains? #{:thead :tr :li} tag)
-                    (str content "\n")
+                                  (contains? #{:p :hr :ul :ol :dl :table :pre :blockquote :aside :canvas
+                                               :center :figure :figcaption :fieldset :div :footer
+                                               :header} tag)
+                                  (str "\n\n" content "\n\n")
 
 
-                    :else
-                    content))
+                                  (contains? #{:thead :tr :li} tag)
+                                  (str content "\n")
+
+                                  :else
+                                  content)]
+                    (some-> content
+                            (string/replace "<!--StartFragment-->" "")
+                            (string/replace "<!--EndFragment-->" ""))))
         single-hiccup-transform
         single-hiccup-transform
         (fn [x]
         (fn [x]
           (cond
           (cond
             (vector? x)
             (vector? x)
             (let [[tag attrs & children] x
             (let [[tag attrs & children] x
                   result (match tag
                   result (match tag
-                                :head nil
-                                :h1 (block-transform 1 children)
-                                :h2 (block-transform 2 children)
-                                :h3 (block-transform 3 children)
-                                :h4 (block-transform 4 children)
-                                :h5 (block-transform 5 children)
-                                :h6 (block-transform 6 children)
-                                :a (let [href (:href attrs)
-                                         label (map-join children)
-                                         has-img-tag? (util/safe-re-find #"\[:img" (str x))]
-                                     (if has-img-tag?
-                                       (export-hiccup x)
-                                       (case format
-                                         :markdown (util/format "[%s](%s)" label href)
-                                         :org (util/format "[[%s][%s]]" href label)
-                                         nil)))
-                                :img (let [src (:src attrs)
-                                           alt (:alt attrs)]
-                                       (case format
-                                         :markdown (util/format "![%s](%s)" alt src)
-                                         :org (util/format "[[%s][%s]]" src alt)
-                                         nil))
-                                :p (util/format "%s"
-                                                (map-join children))
-
-                                :hr (config/get-hr format)
-
-                                (_ :guard #(contains? #{:b :strong
-                                                        :i :em
-                                                        :ins
-                                                        :del
-                                                        :mark} %))
-                                (emphasis-transform tag attrs children)
-
-                                :code (if @*inside-pre?
-                                        (map-join children)
-                                        (let [pattern (config/get-code format)]
-                                          (str " "
-                                               (str pattern (first children) pattern)
-                                               " ")))
-
-                                :pre
-                                (do
-                                  (reset! *inside-pre? true)
-                                  (let [content (string/trim (doall (map-join children)))]
-                                    (reset! *inside-pre? false)
-                                    (case format
-                                      :markdown (if (util/starts-with? content "```")
-                                                  content
-                                                  (str "```\n" content "\n```"))
-                                      :org (if (util/starts-with? content "#+BEGIN_SRC")
+                           :head nil
+                           :h1 (block-transform 1 children)
+                           :h2 (block-transform 2 children)
+                           :h3 (block-transform 3 children)
+                           :h4 (block-transform 4 children)
+                           :h5 (block-transform 5 children)
+                           :h6 (block-transform 6 children)
+                           :a (let [href (:href attrs)
+                                    label (or (map-join children) "")
+                                    has-img-tag? (util/safe-re-find #"\[:img" (str x))]
+                                (if has-img-tag?
+                                  (export-hiccup x)
+                                  (case format
+                                    :markdown (util/format "[%s](%s)" label href)
+                                    :org (util/format "[[%s][%s]]" href label)
+                                    nil)))
+                           :img (let [src (:src attrs)
+                                      alt (or (:alt attrs) "")]
+                                  (case format
+                                    :markdown (util/format "![%s](%s)" alt src)
+                                    :org (util/format "[[%s][%s]]" src alt)
+                                    nil))
+                           :p (util/format "%s"
+                                           (map-join children))
+
+                           :hr (config/get-hr format)
+
+                           (_ :guard #(contains? #{:b :strong
+                                                   :i :em
+                                                   :ins
+                                                   :del
+                                                   :mark
+                                                   :span} %))
+                           (emphasis-transform tag attrs children)
+
+                           :code (if @*inside-pre?
+                                   (map-join children)
+                                   (let [pattern (config/get-code format)]
+                                     (str " "
+                                          (str pattern (first children) pattern)
+                                          " ")))
+
+                           :pre
+                           (do
+                             (reset! *inside-pre? true)
+                             (let [content (string/trim (doall (map-join children)))]
+                               (reset! *inside-pre? false)
+                               (case format
+                                 :markdown (if (util/starts-with? content "```")
                                              content
                                              content
-                                             (util/format "#+BEGIN_SRC\n%s\n#+END_SRC" content))
-                                      nil)))
-
-                                :blockquote
-                                (case format
-                                  :markdown (str "> " (map-join children))
-                                  :org (util/format "#+BEGIN_QUOTE\n%s\n#+END_QUOTE" (map-join children))
-                                  nil)
-
-                                :li
-                                (str "- " (map-join children))
-
-                                :dt
-                                (case format
-                                  :org (str "- " (map-join children) " ")
-                                  :markdown (str (map-join children) "\n")
-                                  nil)
-
-                                :dd
-                                (case format
-                                  :markdown (str ": " (map-join children) "\n")
-                                  :org (str ":: " (map-join children) "\n")
-                                  nil)
-
-                                :thead
-                                (case format
-                                  :markdown (let [columns (count (last (first children)))]
-                                              (str
-                                               (map-join children)
-                                               (str "| " (string/join " | "
-                                                                      (repeat columns "----"))
-                                                    " |")))
-                                  :org (let [columns (count (last (first children)))]
+                                             (str "```\n" content "\n```"))
+                                 :org (if (util/starts-with? content "#+BEGIN_SRC")
+                                        content
+                                        (util/format "#+BEGIN_SRC\n%s\n#+END_SRC" content))
+                                 nil)))
+
+                           :blockquote
+                           (case format
+                             :markdown (str "> " (map-join children))
+                             :org (util/format "#+BEGIN_QUOTE\n%s\n#+END_QUOTE" (map-join children))
+                             nil)
+
+                           :li
+                           (str "- " (map-join children))
+
+                           :dt
+                           (case format
+                             :org (str "- " (map-join children) " ")
+                             :markdown (str (map-join children) "\n")
+                             nil)
+
+                           :dd
+                           (case format
+                             :markdown (str ": " (map-join children) "\n")
+                             :org (str ":: " (map-join children) "\n")
+                             nil)
+
+                           :thead
+                           (case format
+                             :markdown (let [columns (count (last (first children)))]
                                          (str
                                          (str
                                           (map-join children)
                                           (map-join children)
-                                          (str "|" (string/join "+"
-                                                                (repeat columns "----"))
-                                               "|")))
-                                  nil)
-                                :tr
-                                (str "| "
-                                     (->> (map transform-fn children)
-                                          (string/join " | "))
-                                     " |")
-
-                                (_ :guard #(contains? #{:aside :center :figure :figcaption :fieldset :footer :header} %))
-                                (export-hiccup x)
-
-                                :else (map-join children))]
+                                          (str "| " (string/join " | "
+                                                                 (repeat columns "----"))
+                                               " |")))
+                             :org (let [columns (count (last (first children)))]
+                                    (str
+                                     (map-join children)
+                                     (str "|" (string/join "+"
+                                                           (repeat columns "----"))
+                                          "|")))
+                             nil)
+                           :tr
+                           (str "| "
+                                (->> (map #(transform-fn % (assoc opts :in-table? true)) children)
+                                     (string/join " | "))
+                                " |")
+
+                           (_ :guard #(contains? #{:aside :center :figure :figcaption :fieldset :footer :header} %))
+                           (export-hiccup x)
+
+                           :else (map-join children))]
               (wrapper tag result))
               (wrapper tag result))
 
 
             (string? x)
             (string? x)
@@ -181,7 +236,7 @@
 
 
 (defn hiccup->doc
 (defn hiccup->doc
   [format hiccup]
   [format hiccup]
-  (let [s (hiccup->doc-inner format hiccup)]
+  (let [s (hiccup->doc-inner format hiccup {})]
     (if (string/blank? s)
     (if (string/blank? s)
       ""
       ""
       (-> s
       (-> s
@@ -196,7 +251,7 @@
                      (goog.string.unescapeEntities f)
                      (goog.string.unescapeEntities f)
                      f)) hiccup))
                      f)) hiccup))
 
 
-(defn parse
+(defn convert
   [format html]
   [format html]
   (when-not (string/blank? html)
   (when-not (string/blank? html)
     (let [hiccup (hickory/as-hiccup (hickory/parse html))
     (let [hiccup (hickory/as-hiccup (hickory/parse html))

+ 12 - 8
src/main/frontend/extensions/pdf/assets.cljs

@@ -7,6 +7,7 @@
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.util.page-property :as page-property]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.config :as gp-config]
@@ -154,14 +155,14 @@
         page-name (str "hls__" page-name)
         page-name (str "hls__" page-name)
         page (db-model/get-page page-name)
         page (db-model/get-page page-name)
         url (:url pdf-current)
         url (:url pdf-current)
-        format (state/get-preferred-format)]
+        format (state/get-preferred-format)
+        repo-dir (config/get-repo-dir (state/get-current-repo))
+        asset-dir (util/node-path.join repo-dir gp-config/local-assets-dir)
+        url (if (string/includes? url asset-dir)
+              (str ".." (last (string/split url repo-dir)))
+              url)]
     (if-not page
     (if-not page
-      (let [repo-dir (config/get-repo-dir (state/get-current-repo))
-            asset-dir (util/node-path.join repo-dir gp-config/local-assets-dir)
-            url (if (string/includes? url asset-dir)
-                  (str ".." (last (string/split url repo-dir)))
-                  url)
-            label (:filename pdf-current)]
+      (let [label (:filename pdf-current)]
         (page-handler/create! page-name {:redirect?        false :create-first-block? false
         (page-handler/create! page-name {:redirect?        false :create-first-block? false
                                          :split-namespace? false
                                          :split-namespace? false
                                          :format           format
                                          :format           format
@@ -175,7 +176,10 @@
                                                                          url)
                                                                          url)
                                                             :file-path url}})
                                                             :file-path url}})
         (db-model/get-page page-name))
         (db-model/get-page page-name))
-      page)))
+
+      ;; try to update file path
+      (page-property/add-property! page-name :file-path url))
+    page))
 
 
 (defn create-ref-block!
 (defn create-ref-block!
   [{:keys [id content page]}]
   [{:keys [id content page]}]

+ 4 - 0
src/main/frontend/extensions/pdf/pdf.css

@@ -684,6 +684,10 @@ body.is-pdf-active {
   #main-container.is-left-sidebar-open {
   #main-container.is-left-sidebar-open {
     padding-left: unset;
     padding-left: unset;
   }
   }
+
+  #left-menu {
+    display: none;
+  }
 }
 }
 
 
 /* overrides for pdf_viewer.css from PDF.JS web viewer */
 /* overrides for pdf_viewer.css from PDF.JS web viewer */

+ 6 - 3
src/main/frontend/extensions/sci.cljs

@@ -1,5 +1,6 @@
 (ns frontend.extensions.sci
 (ns frontend.extensions.sci
-  (:require [sci.core :as sci]))
+  (:require [sci.core :as sci]
+            [frontend.util :as util]))
 
 
 ;; Some helpers
 ;; Some helpers
 (def sum (partial apply +))
 (def sum (partial apply +))
@@ -13,7 +14,9 @@
     (sci/eval-string s {:bindings {'sum sum
     (sci/eval-string s {:bindings {'sum sum
                                    'average average
                                    'average average
                                    'parseFloat js/parseFloat
                                    'parseFloat js/parseFloat
-                                   'isNaN js/isNaN}})
+                                   'isNaN js/isNaN
+                                   'log js/console.log
+                                   'pprint util/pp-str}})
     (catch js/Error e
     (catch js/Error e
       (println "Query: sci eval failed:")
       (println "Query: sci eval failed:")
       (js/console.error e))))
       (js/console.error e))))
@@ -29,4 +32,4 @@
    [:div.results.mt-1
    [:div.results.mt-1
     [:pre.code
     [:pre.code
      (let [result (eval-string code)]
      (let [result (eval-string code)]
-       (str result))]]])
+       (str result))]]])

+ 1 - 1
src/main/frontend/extensions/zotero/extractor.cljs

@@ -140,7 +140,7 @@
 (defmethod extract "note"
 (defmethod extract "note"
   [item]
   [item]
   (let [note-html (-> item :data :note)]
   (let [note-html (-> item :data :note)]
-    (html-parser/parse :markdown note-html)))
+    (html-parser/convert :markdown note-html)))
 
 
 (defn zotero-imported-file-macro [item-key filename]
 (defn zotero-imported-file-macro [item-key filename]
   (util/format "{{zotero-imported-file %s, %s}}" item-key (pr-str filename)))
   (util/format "{{zotero-imported-file %s, %s}}" item-key (pr-str filename)))

+ 0 - 5
src/main/frontend/format.cljs

@@ -3,14 +3,9 @@
             [frontend.format.adoc :refer [->AdocMode]]
             [frontend.format.adoc :refer [->AdocMode]]
             [frontend.format.protocol :as protocol]
             [frontend.format.protocol :as protocol]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]))
             [clojure.string :as string]))
 
 
-;; TODO: Properly fix this circular dependency:
-;; mldoc/->edn > text/parse-property > mldoc/link? ->mldoc/inline->edn + mldoc/default-config
-(set! gp-mldoc/parse-property text/parse-property)
-
 (defonce mldoc-record (->MldocMode))
 (defonce mldoc-record (->MldocMode))
 (defonce adoc-record (->AdocMode))
 (defonce adoc-record (->AdocMode))
 
 

+ 2 - 1
src/main/frontend/format/block.cljs

@@ -6,6 +6,7 @@
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.format :as format]
             [frontend.format :as format]
             [frontend.state :as state]
             [frontend.state :as state]
+            [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.mldoc :as gp-mldoc]))
             [logseq.graph-parser.mldoc :as gp-mldoc]))
 
 
@@ -15,7 +16,7 @@
   (gp-block/extract-blocks blocks content with-id? format
   (gp-block/extract-blocks blocks content with-id? format
                            {:user-config (state/get-config)
                            {:user-config (state/get-config)
                             :block-pattern (config/get-block-pattern format)
                             :block-pattern (config/get-block-pattern format)
-                            :supported-formats (config/supported-formats)
+                            :supported-formats (gp-config/supported-formats)
                             :db (db/get-db (state/get-current-repo))
                             :db (db/get-db (state/get-current-repo))
                             :date-formatter (state/get-date-formatter)}))
                             :date-formatter (state/get-date-formatter)}))
 
 

Some files were not shown because too many files changed in this diff