Browse Source

Merge branch 'master' into gesture-support-on-block

llcc 3 years ago
parent
commit
ee3b578d99
100 changed files with 1634 additions and 2048 deletions
  1. 3 1
      .carve/config.edn
  2. 4 2
      .carve/ignore
  3. 17 2
      .clj-kondo/config.edn
  4. 3 1
      .github/workflows/build.yml
  5. 2 2
      android/app/build.gradle
  6. 1 0
      android/app/capacitor.build.gradle
  7. 3 0
      android/app/src/main/assets/capacitor.config.json
  8. 4 0
      android/app/src/main/assets/capacitor.plugins.json
  9. 33 25
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  10. 3 0
      android/capacitor.settings.gradle
  11. 4 2
      bb.edn
  12. 4 0
      capacitor.config.ts
  13. 5 5
      deps.edn
  14. 3 3
      docs/dev-practices.md
  15. 3 4
      e2e-tests/code-editing.spec.ts
  16. 3 0
      externs.js
  17. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  18. 3 33
      ios/App/App/AppDelegate.swift
  19. 2 26
      ios/App/App/FileSync/Extensions.swift
  20. 53 10
      ios/App/App/FileSync/FileSync.swift
  21. 35 22
      ios/App/App/FsWatcher.swift
  22. 3 0
      ios/App/App/capacitor.config.json
  23. 1 0
      ios/App/Podfile
  24. 1 1
      ios/App/ShareViewController/ShareViewController.swift
  25. 112 51
      libs/src/LSPlugin.core.ts
  26. 34 19
      libs/src/LSPlugin.ts
  27. 4 3
      libs/src/LSPlugin.user.ts
  28. 5 10
      libs/src/helpers.ts
  29. 3 2
      package.json
  30. 5 0
      resources/css/common.css
  31. 0 0
      resources/js/lsplugin.core.js
  32. 3 3
      resources/package.json
  33. 17 0
      scripts/src/logseq/tasks/dev.clj
  34. 6 3
      shadow-cljs.edn
  35. 51 0
      src/electron/electron/backup_file.cljs
  36. 11 9
      src/electron/electron/fs_watcher.cljs
  37. 8 34
      src/electron/electron/handler.cljs
  38. 13 13
      src/electron/electron/utils.cljs
  39. 6 12
      src/main/frontend/commands.cljs
  40. 253 156
      src/main/frontend/components/block.cljs
  41. 14 9
      src/main/frontend/components/block.css
  42. 5 5
      src/main/frontend/components/content.cljs
  43. 5 100
      src/main/frontend/components/editor.cljs
  44. 0 30
      src/main/frontend/components/editor.css
  45. 2 2
      src/main/frontend/components/file.cljs
  46. 3 5
      src/main/frontend/components/header.cljs
  47. 4 1
      src/main/frontend/components/header.css
  48. 1 1
      src/main/frontend/components/hierarchy.cljs
  49. 6 4
      src/main/frontend/components/journal.cljs
  50. 2 2
      src/main/frontend/components/onboarding/setups.cljs
  51. 21 23
      src/main/frontend/components/page.cljs
  52. 6 6
      src/main/frontend/components/page_menu.cljs
  53. 72 47
      src/main/frontend/components/plugins.cljs
  54. 3 8
      src/main/frontend/components/plugins.css
  55. 8 13
      src/main/frontend/components/reference.cljs
  56. 3 3
      src/main/frontend/components/repo.cljs
  57. 1 2
      src/main/frontend/components/search.cljs
  58. 1 1
      src/main/frontend/components/select.cljs
  59. 6 6
      src/main/frontend/components/settings.cljs
  60. 48 18
      src/main/frontend/components/sidebar.cljs
  61. 1 0
      src/main/frontend/components/theme.cljs
  62. 2 2
      src/main/frontend/components/widgets.cljs
  63. 12 11
      src/main/frontend/config.cljs
  64. 17 36
      src/main/frontend/date.cljs
  65. 3 3
      src/main/frontend/db/conn.cljs
  66. 2 3
      src/main/frontend/db/debug.cljs
  67. 85 9
      src/main/frontend/db/model.cljs
  68. 10 10
      src/main/frontend/db/query_dsl.cljs
  69. 5 6
      src/main/frontend/db/query_react.cljs
  70. 1 2
      src/main/frontend/db/react.cljs
  71. 1 2
      src/main/frontend/db/utils.cljs
  72. 92 91
      src/main/frontend/dicts.cljc
  73. 2 2
      src/main/frontend/diff.cljs
  74. 1 1
      src/main/frontend/encrypt.cljs
  75. 1 1
      src/main/frontend/extensions/code.cljs
  76. 1 1
      src/main/frontend/extensions/excalidraw.cljs
  77. 1 2
      src/main/frontend/extensions/html_parser.cljs
  78. 3 4
      src/main/frontend/extensions/slide.cljs
  79. 1 1
      src/main/frontend/extensions/video/youtube.cljs
  80. 3 4
      src/main/frontend/external/roam.cljs
  81. 10 19
      src/main/frontend/format.cljs
  82. 18 653
      src/main/frontend/format/block.cljs
  83. 14 209
      src/main/frontend/format/mldoc.cljs
  84. 5 5
      src/main/frontend/fs.cljs
  85. 125 53
      src/main/frontend/fs/sync.cljs
  86. 1 1
      src/main/frontend/fs/watcher_handler.cljs
  87. 2 2
      src/main/frontend/handler.cljs
  88. 3 3
      src/main/frontend/handler/block.cljs
  89. 81 90
      src/main/frontend/handler/editor.cljs
  90. 1 4
      src/main/frontend/handler/editor/lifecycle.cljs
  91. 25 13
      src/main/frontend/handler/events.cljs
  92. 5 3
      src/main/frontend/handler/export.cljs
  93. 4 2
      src/main/frontend/handler/external.cljs
  94. 16 7
      src/main/frontend/handler/file.cljs
  95. 97 14
      src/main/frontend/handler/file_sync.cljs
  96. 2 3
      src/main/frontend/handler/graph.cljs
  97. 15 12
      src/main/frontend/handler/page.cljs
  98. 18 10
      src/main/frontend/handler/plugin.cljs
  99. 4 4
      src/main/frontend/handler/repo.cljs
  100. 5 6
      src/main/frontend/handler/route.cljs

+ 3 - 1
.carve/config.edn

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

+ 4 - 2
.carve/ignore

@@ -29,7 +29,7 @@ frontend.extensions.zotero.api/item
 ;; For repl
 frontend.external.roam/reset-state!
 ;; For repl
-frontend.format.mldoc/ast-export-markdown
+logseq.graph-parser.mldoc/ast-export-markdown
 ;; Protocol fn wrapper that could be used
 frontend.fs/readdir
 ;; Referenced in TODO
@@ -72,5 +72,7 @@ frontend.util/trace!
 frontend.util.pool/terminate-pool!
 ;; Repl fn
 frontend.util.property/add-page-properties
-;; Used by shadow
+;; Test runner used by shadow
 frontend.test.node-test-runner/main
+;; Test runner for nbb
+logseq.graph-parser.nbb-test-runner/run-tests

+ 17 - 2
.clj-kondo/config.edn

@@ -2,7 +2,13 @@
  {:unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
-                                object]}
+                                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]}
   ;; TODO:lint: Remove node-path excludes once we have a cleaner api
   :unresolved-var {:exclude [frontend.util/node-path.basename
                              frontend.util/node-path.dirname
@@ -19,9 +25,18 @@
              frontend.db.react react
              frontend.db.query-react query-react
              frontend.util util
+             frontend.util.property property
              frontend.config config
+             frontend.format.mldoc mldoc
+             frontend.format.block block
+             frontend.handler.extract extract
+             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.config gp-config}}}
+             logseq.graph-parser.property gp-property
+             logseq.graph-parser.config gp-config
+             logseq.graph-parser.date-time-util date-time-util}}}
 
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
                          rum.core/defcs hooks.rum/defcs}}

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

@@ -74,11 +74,13 @@ jobs:
       - name: Fetch yarn deps
         run: yarn install --frozen-lockfile
 
-      - name: Run ClojureScript test
+      - name: Run ClojureScript tests
         run: |
           yarn cljs:test
           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
       - name: Load nbb compatible namespaces
         run: bb test:load-nbb-compatible-namespaces

+ 2 - 2
android/app/build.gradle

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

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

@@ -15,6 +15,7 @@ dependencies {
     implementation project(':capacitor-filesystem')
     implementation project(':capacitor-haptics')
     implementation project(':capacitor-keyboard')
+    implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
     implementation project(':capacitor-voice-recorder')

+ 3 - 0
android/app/src/main/assets/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

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

@@ -23,6 +23,10 @@
 		"pkg": "@capacitor/keyboard",
 		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
 	},
+	{
+		"pkg": "@capacitor/share",
+		"classpath": "com.capacitorjs.plugins.share.SharePlugin"
+	},
 	{
 		"pkg": "@capacitor/splash-screen",
 		"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"

+ 33 - 25
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -1,5 +1,7 @@
 package com.logseq.app;
 
+import android.annotation.SuppressLint;
+import android.os.Build;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructStat;
@@ -26,7 +28,6 @@ public class FsWatcher extends Plugin {
 
     List<SingleFileObserver> observers;
     private String mPath;
-    private Uri mPathUri;
 
     @Override
     public void load() {
@@ -35,17 +36,23 @@ public class FsWatcher extends Plugin {
 
     @PluginMethod()
     public void watch(PluginCall call) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            call.reject("Android version not supported");
+            return;
+        }
         String pathParam = call.getString("path");
         // check file:// or no scheme uris
         Uri u = Uri.parse(pathParam);
         Log.i("FsWatcher", "watching " + u);
         if (u.getScheme() == null || u.getScheme().equals("file")) {
-            File pathObj = new File(u.getPath());
-            if (pathObj == null) {
+            File pathObj;
+            try {
+                pathObj = new File(u.getPath());
+            } catch (Exception e) {
                 call.reject("invalid watch path: " + pathParam);
                 return;
             }
-            mPathUri = Uri.fromFile(pathObj);
+
             mPath = pathObj.getAbsolutePath();
 
             int mask = FileObserver.CLOSE_WRITE |
@@ -56,15 +63,16 @@ public class FsWatcher extends Plugin {
                 call.reject("already watching");
                 return;
             }
-            observers = new ArrayList<SingleFileObserver>();
+            observers = new ArrayList<>();
             observers.add(new SingleFileObserver(pathObj, mask));
 
             // NOTE: only watch first level of directory
             File[] files = pathObj.listFiles();
             if (files != null) {
-                for (int i = 0; i < files.length; ++i) {
-                    if (files[i].isDirectory() && !files[i].getName().startsWith(".")) {
-                        observers.add(new SingleFileObserver(files[i], mask));
+                for (File file : files) {
+                    String filename = file.getName();
+                    if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("node_modules")) {
+                        observers.add(new SingleFileObserver(file, mask));
                     }
                 }
             }
@@ -103,13 +111,14 @@ public class FsWatcher extends Plugin {
         }
         File[] files = pathObj.listFiles();
         if (files != null) {
-            for (int i = 0; i < files.length; ++i) {
-                if (files[i].isDirectory() && !files[i].getName().startsWith(".") && !files[i].getName().equals("bak")) {
-                    this.initialNotify(files[i], maxDepth - 1);
-                } else if (files[i].isFile()
-                        && Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$",
-                                files[i].getName())) {
-                    this.onObserverEvent(FileObserver.CREATE, files[i].getAbsolutePath());
+            for (File file : files) {
+                String filename = file.getName();
+                if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) {
+                    this.initialNotify(file, maxDepth - 1);
+                } else if (file.isFile()
+                        && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$",
+                        file.getName())) {
+                    this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath());
                 }
             }
         }
@@ -132,9 +141,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -145,9 +152,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -172,7 +177,7 @@ public class FsWatcher extends Plugin {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 
         byte[] buffer = new byte[1024];
-        int length = 0;
+        int length;
 
         while ((length = inputStream.read(buffer)) != -1) {
             outputStream.write(buffer, 0, length);
@@ -183,22 +188,25 @@ public class FsWatcher extends Plugin {
     }
 
     public static JSObject getFileStat(final String path) throws ErrnoException {
+        File file = new File(path);
         StructStat stat = Os.stat(path);
         JSObject obj = new JSObject();
         obj.put("atime", stat.st_atime);
         obj.put("mtime", stat.st_mtime);
         obj.put("ctime", stat.st_ctime);
+        obj.put("size", file.length());
         return obj;
     }
 
     private class SingleFileObserver extends FileObserver {
-        private String mPath;
+        private final String mPath;
 
         public SingleFileObserver(String path, int mask) {
             super(path, mask);
             mPath = path;
         }
 
+        @SuppressLint("NewApi")
         public SingleFileObserver(File path, int mask) {
             super(path, mask);
             mPath = path.getAbsolutePath();
@@ -206,9 +214,9 @@ public class FsWatcher extends Plugin {
 
         @Override
         public void onEvent(int event, String path) {
-            if (path != null) {
+            if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
-                if (Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$", path)) {
+                if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     FsWatcher.this.onObserverEvent(event, fullPath);
                 }

+ 3 - 0
android/capacitor.settings.gradle

@@ -20,6 +20,9 @@ project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/
 include ':capacitor-keyboard'
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 
+include ':capacitor-share'
+project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
+
 include ':capacitor-splash-screen'
 project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
 

+ 4 - 2
bb.edn

@@ -2,8 +2,7 @@
  :deps
  {org.babashka/spec.alpha
   {:git/url "https://github.com/babashka/spec.alpha"
-   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}
-  medley/medley {:mvn/version "1.3.0"}}
+   :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}}
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
  :tasks
@@ -24,6 +23,9 @@
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
 
+  dev:lint
+  logseq.tasks.dev/lint
+
   test:load-nbb-compatible-namespaces
   logseq.tasks.nbb/load-compatible-namespaces
 

+ 4 - 0
capacitor.config.ts

@@ -13,6 +13,10 @@ const config: CapacitorConfig = {
             splashImmersive: false,
             backgroundColor: "#002b36"
         },
+
+        Keyboard: {
+            resize: "none"
+        }
     },
     ios: {
         scheme: "Logseq"

+ 5 - 5
deps.edn

@@ -7,7 +7,7 @@
   borkdude/rewrite-edn                  {:git/url "https://github.com/borkdude/rewrite-edn"
                                          :sha     "edd87dc7f045f28d7afcbfc44bc0f0a2683dde62"}
   funcool/promesa                       {:mvn/version "4.0.2"}
-  medley/medley                         {:mvn/version "1.2.0"}
+  medley/medley                         {:mvn/version "1.4.0"}
   metosin/reitit-frontend               {:mvn/version "0.3.10"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}
   prismatic/dommy                       {:mvn/version "1.1.0"}
@@ -20,9 +20,9 @@
   hickory/hickory                       {:git/url "https://github.com/logseq/hickory"
                                          :sha     "9c2c2f1fc2c45efaad906e0faabc3201278deeaa"}
   hiccups/hiccups                       {:mvn/version "0.3.0"}
-  tongue/tongue                         {:mvn/version "0.2.9"}
+  tongue/tongue                         {:mvn/version "0.4.4"}
   org.clojure/core.async                {:mvn/version "1.3.610"}
-  thheller/shadow-cljs                  {:mvn/version "2.17.5"}
+  thheller/shadow-cljs                  {:mvn/version "2.19.0"}
   expound/expound                       {:mvn/version "0.8.6"}
   com.lambdaisland/glogi                {:mvn/version "1.1.144"}
   binaryage/devtools                    {:mvn/version "1.0.5"}
@@ -33,14 +33,14 @@
   org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/tools.namespace      {:mvn/version "0.2.11"}
                                 cider/cider-nrepl                {:mvn/version "0.26.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            :test {:extra-paths ["src/test/"]
-                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.10.891"}
+                  :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/test.check           {:mvn/version "1.1.1"}
                                 pjstadig/humane-test-output      {:mvn/version "0.11.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}

+ 3 - 3
docs/dev-practices.md

@@ -101,9 +101,9 @@ For this workflow:
   1. Add `^:focus` metadata flags to tests e.g. `(deftest ^:focus test-name ...)`.
   2. In another shell, run `node static/tests.js -i focus` to only run those
   tests. To run all tests except those tests run `node static/tests.js -e focus`.
-3. Or focus namespaces: Using the regex option `-r`, run tests for `frontend.text-test` with `node static/tests.js -r text`.
+3. Or focus namespaces: Using the regex option `-r`, run tests for `frontend.util.page-property-test` with `node static/tests.js -r page-property`.
 
-Multiple options can be specified to AND selections. For example, to run all `frontend.text-test` tests except for the focused one: `node static/tests.js -r text -e focus`
+Multiple options can be specified to AND selections. For example, to run all `frontend.util.page-property-test` tests except for the focused one: `node static/tests.js -r page-property -e focus`
 
 For help on more options, run `node static/tests.js -h`.
 
@@ -114,7 +114,7 @@ shadow-cljs watch test --config-merge '{:autorun true}'`. The test output may
 appear where shadow-cljs was first invoked e.g. where `yarn watch` is running.
 Specific namespace(s) can be auto run with the `:ns-regexp` option e.g. `npx
 shadow-cljs watch test --config-merge '{:autorun true :ns-regexp
-"frontend.text-test"}'`.
+"frontend.util.page-property-test"}'`.
 
 ## Logging
 

+ 3 - 4
e2e-tests/code-editing.spec.ts

@@ -164,7 +164,7 @@ test('multiple code block', async ({ page }) => {
   await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
 
   await page.waitForTimeout(500)
-  await page.click('.CodeMirror pre >> nth=1')
+  await page.click('.CodeMirror >> nth=1 >> pre')
   await page.waitForTimeout(500)
 
   await page.type('.CodeMirror textarea >> nth=1', '\n  :key-test 日本語\n', { strict: true })
@@ -191,11 +191,10 @@ test('click outside to exit', async ({ page }) => {
   expect(await page.inputValue('.block-editor textarea')).toBe('Header ``Click``\n```\n  ABC  DEF\n  GHI\n```')
 })
 
-test('click language label to exit #3463', async ({ page }) => {
+test('click language label to exit #3463', async ({ page, block }) => {
   await createRandomPage(page)
 
-  await page.press('.block-editor textarea', 'Enter')
-  await page.waitForTimeout(200)
+  await block.enterNext();
 
   await page.fill('.block-editor textarea', '```cpp\n```')
   await page.waitForTimeout(200)

+ 3 - 0
externs.js

@@ -127,6 +127,9 @@ dummy.getNodesObjects = function() {};
 dummy.getEdgesObjects = function() {};
 dummy.alphaTarget = function() {};
 dummy.restart = function() {};
+dummy.observe = function() {};
+dummy.contentRect = function() {};
+dummy.height = function() {};
 
 /**
  * @typedef {{

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

@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 3 - 33
ios/App/App/AppDelegate.swift

@@ -1,19 +1,14 @@
 import UIKit
 import Capacitor
-import SendIntent
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate {
 
     var window: UIWindow?
-    let store = ShareStore.store
     
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
-        DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
-            NotificationCenter.default
-                .post(name: Notification.Name("triggerSendIntent"), object: nil )
-        }
+        
         return true
     }
     
@@ -33,6 +28,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func applicationDidBecomeActive(_ application: UIApplication) {
         // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+        
+        
     }
 
     func applicationWillTerminate(_ application: UIApplication) {
@@ -45,33 +42,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
             if CAPBridge.handleOpenUrl(url, options) {
                 success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
             }
-            
-            guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
-                  let params = components.queryItems else {
-                      return false
-                  }
-            let titles = params.filter { $0.name == "title" }
-            let descriptions = params.filter { $0.name == "description" }
-            let types = params.filter { $0.name == "type" }
-            let urls = params.filter { $0.name == "url" }
-            
-            store.shareItems.removeAll()
-        
-            if(titles.count > 0){
-                for index in 0...titles.count-1 {
-                    var shareItem: JSObject = JSObject()
-                    shareItem["title"] = titles[index].value!
-                    shareItem["description"] = descriptions[index].value!
-                    shareItem["type"] = types[index].value!
-                    shareItem["url"] = urls[index].value!
-                    store.shareItems.append(shareItem)
-                }
-            }
-            
-            store.processed = false
-            
-            NotificationCenter.default.post(name: Notification.Name("triggerSendIntent"), object: nil )
-            
             return success
         }
 

+ 2 - 26
ios/App/App/FileSync/Extensions.swift

@@ -8,11 +8,6 @@
 import Foundation
 import CryptoKit
 
-
-import var CommonCrypto.CC_MD5_DIGEST_LENGTH
-import func CommonCrypto.CC_MD5
-import typealias CommonCrypto.CC_LONG
-
 // via https://github.com/krzyzanowskim/CryptoSwift
 extension Array where Element == UInt8 {
   public init(hex: String) {
@@ -120,27 +115,8 @@ extension Data {
 
 extension String {
     var MD5: String {
-        // TODO: incremental hash
-        if #available(iOS 13.0, *) {
-            let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
-            return computed.map { String(format: "%02hhx", $0) }.joined()
-        } else {
-            // Fallback on earlier versions, no CryptoKit
-            let length = Int(CC_MD5_DIGEST_LENGTH)
-            let messageData = self.data(using:.utf8)!
-            var digestData = Data(count: length)
-            
-            _ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in
-                messageData.withUnsafeBytes { messageBytes -> UInt8 in
-                    if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress {
-                        let messageLength = CC_LONG(messageData.count)
-                        CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
-                    }
-                    return 0
-                }
-            }
-            return digestData.map { String(format: "%02hhx", $0) }.joined()
-        }
+        let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
+        return computed.map { String(format: "%02hhx", $0) }.joined()
     }
     
     func encodeAsFname() -> String {

+ 53 - 10
ios/App/App/FileSync/FileSync.swift

@@ -17,6 +17,49 @@ var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
 var BUCKET: String = "logseq-file-sync-bucket"
 var REGION: String = "us-east-2"
 
+
+public struct SyncMetadata: CustomStringConvertible, Equatable {
+    var md5: String
+    var size: Int
+
+    public init?(of fileURL: URL) {
+        do {
+            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey])
+            guard fileAttributes.isRegularFile! else {
+                return nil
+            }
+            size = fileAttributes.fileSize ?? 0
+            
+            // incremental MD5sum
+            let bufferSize = 1024 * 1024
+            let file = try FileHandle(forReadingFrom: fileURL)
+            defer {
+                file.closeFile()
+            }
+            var ctx = Insecure.MD5.init()
+            while autoreleasepool(invoking: {
+                let data = file.readData(ofLength: bufferSize)
+                if data.count > 0 {
+                    ctx.update(data: data)
+                    return true // continue
+                } else {
+                    return false // eof
+                }
+            }) {}
+            
+            let computed = ctx.finalize()
+            md5 = computed.map { String(format: "%02hhx", $0) }.joined()
+        } catch {
+            return nil
+        }
+    }
+
+    public var description: String {
+        return "SyncMetadata(md5=\(md5), size=\(size))"
+    }
+}
+
+
 // MARK: FileSync Plugin
 
 @objc(FileSync)
@@ -69,16 +112,16 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             return
         }
         
-        var fileMd5Digests: [String: [String: Any]] = [:]
+        var fileMetadataDict: [String: [String: Any]] = [:]
         for filePath in filePaths {
             let url = baseURL.appendingPathComponent(filePath)
-            if let content = try? String(contentsOf: url, encoding: .utf8) {
-                fileMd5Digests[filePath] = ["md5": content.MD5,
-                                            "size": content.lengthOfBytes(using: .utf8)]
+            if let meta = SyncMetadata(of: url) {
+                fileMetadataDict[filePath] = ["md5": meta.md5,
+                                              "size": meta.size]
             }
         }
         
-        call.resolve(["result": fileMd5Digests])
+        call.resolve(["result": fileMetadataDict])
     }
     
     @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
@@ -88,21 +131,21 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                   return
               }
         
-        var fileMd5Digests: [String: [String: Any]] = [:]
+        var fileMetadataDict: [String: [String: Any]] = [:]
         if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
             
             for case let fileURL as URL in enumerator {
                 if !fileURL.isSkipped() {
-                    if let content = try? String(contentsOf: fileURL, encoding: .utf8) {
-                        fileMd5Digests[fileURL.relativePath(from: baseURL)!] = ["md5": content.MD5,
-                                                                                "size": content.lengthOfBytes(using: .utf8)]
+                    if let meta = SyncMetadata(of: fileURL) {
+                        fileMetadataDict[fileURL.relativePath(from: baseURL)!] = ["md5": meta.md5,
+                                                                                  "size": meta.size]
                     }
                 } else if fileURL.isICloudPlaceholder() {
                     try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
                 }
             }
         }
-        call.resolve(["result": fileMd5Digests])
+        call.resolve(["result": fileMetadataDict])
     }
     
     

+ 35 - 22
ios/App/App/FsWatcher.swift

@@ -52,23 +52,20 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                   ])
-        case .Add:
-            let content = try? String(contentsOf: url, encoding: .utf8)
-            self.notifyListeners("watcher", data: ["event": "add",
+        case .Add, .Change:
+            var content: String? = nil
+            if url.shouldNotifyWithContent() {
+                content = try? String(contentsOf: url, encoding: .utf8)
+            }
+            self.notifyListeners("watcher", data: ["event": event.description,
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                    "content": content as Any,
-                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
-                                                            "ctime": metadata?.creationTimestamp]
+                                                   "stat": ["mtime": metadata?.contentModificationTimestamp ?? 0,
+                                                            "ctime": metadata?.creationTimestamp ?? 0,
+                                                            "size": metadata?.fileSize as Any]
                                                   ])
-        case .Change:
-            let content = try? String(contentsOf: url, encoding: .utf8)
-            self.notifyListeners("watcher", data: ["event": "change",
-                                                   "dir": baseUrl?.description as Any,
-                                                   "path": url.description,
-                                                   "content": content as Any,
-                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
-                                                            "ctime": metadata?.creationTimestamp]])
+            
         case .Error:
             // TODO: handle error?
             break
@@ -84,16 +81,18 @@ extension URL {
         if self.lastPathComponent.starts(with: ".") {
             return true
         }
-        // NOTE: used by file-sync
-        if self.lastPathComponent == "graphs-txid.edn" {
+        if self.lastPathComponent == "graphs-txid.edn" || self.lastPathComponent == "broken-config.edn" {
             return true
         }
-        let allowedPathExtensions: Set = ["md", "markdown", "org", "css", "edn", "excalidraw"]
+        return false
+    }
+    
+    func shouldNotifyWithContent() -> Bool {
+        let allowedPathExtensions: Set = ["md", "markdown", "org", "js", "edn", "css", "excalidraw"]
         if allowedPathExtensions.contains(self.pathExtension.lowercased()) {
-            return false
+            return true
         }
-        // skip for other file types
-        return true
+        return false
     }
     
     func isICloudPlaceholder() -> Bool {
@@ -110,13 +109,27 @@ public protocol PollingWatcherDelegate {
     func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?)
 }
 
-public enum PollingWatcherEvent: String {
+public enum PollingWatcherEvent {
     case Add
     case Change
     case Unlink
     case Error
+    
+    var description: String {
+        switch self {
+        case .Add:
+            return "add"
+        case .Change:
+            return "change"
+        case .Unlink:
+            return "unlink"
+        case .Error:
+            return "error"
+        }
+    }
 }
 
+
 public struct SimpleFileMetadata: CustomStringConvertible, Equatable {
     var contentModificationTimestamp: Double
     var creationTimestamp: Double
@@ -192,11 +205,11 @@ public class PollingWatcher {
                 
                 if isDirectory {
                     // NOTE: URL.path won't end with a `/`
-                    if fileURL.path.hasSuffix("/logseq/bak") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
+                    if fileURL.path.hasSuffix("/logseq/bak") || fileURL.path.hasSuffix("/logseq/version-files") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
                         enumerator.skipDescendants()
                     }
                 }
-            
+                
                 if isRegularFile && !fileURL.isSkipped() {
                     if let meta = SimpleFileMetadata(of: fileURL) {
                         newMetaDb[fileURL] = meta

+ 3 - 0
ios/App/App/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

+ 1 - 0
ios/App/Podfile

@@ -15,6 +15,7 @@ def capacitor_pods
   pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
   pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
+  pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'

+ 1 - 1
ios/App/ShareViewController/ShareViewController.swift

@@ -44,7 +44,7 @@ class ShareViewController: UIViewController {
                     value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
             ]
         }.flatMap({ $0 })
-        var urlComps = URLComponents(string: "logseq://")!
+        var urlComps = URLComponents(string: "logseq://shared?")!
         urlComps.queryItems = queryItems
         openURL(urlComps.url!)
     }

+ 112 - 51
libs/src/LSPlugin.core.ts

@@ -3,7 +3,6 @@ import {
   deepMerge,
   setupInjectedStyle,
   genID,
-  setupInjectedTheme,
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
@@ -19,6 +18,7 @@ import {
   IS_DEV,
   cleanInjectedScripts,
   safeSnakeCase,
+  injectTheme,
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -34,11 +34,13 @@ import {
 } from './LSPlugin.caller'
 import {
   ILSPluginThemeManager,
+  LegacyTheme,
   LSPluginPkgConfig,
   SettingSchemaDesc,
   StyleOptions,
   StyleString,
-  ThemeOptions,
+  Theme,
+  ThemeMode,
   UIContainerAttrs,
   UIOptions,
 } from './LSPlugin'
@@ -173,10 +175,13 @@ class PluginLogger extends EventEmitter<'change'> {
 }
 
 interface UserPreferences {
-  theme: ThemeOptions
+  theme: LegacyTheme
+  themes: {
+    mode: ThemeMode
+    light: Theme
+    dark: Theme
+  }
   externals: string[] // external plugin locations
-
-  [key: string]: any
 }
 
 interface PluginLocalOptions {
@@ -310,7 +315,7 @@ function initProviderHandlers(pluginLocal: PluginLocal) {
   let themed = false
 
   // provider:theme
-  pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
+  pluginLocal.on(_('theme'), (theme: Theme) => {
     pluginLocal.themeMgr.registerTheme(pluginLocal.id, theme)
 
     if (!themed) {
@@ -697,7 +702,7 @@ class PluginLocal extends EventEmitter<
     this._options.entry = entry
   }
 
-  async _loadConfigThemes(themes: ThemeOptions[]) {
+  async _loadConfigThemes(themes: Theme[]) {
     themes.forEach((options) => {
       if (!options.url) return
 
@@ -1123,6 +1128,7 @@ class LSPluginCore
     | 'unregistered'
     | 'theme-changed'
     | 'theme-selected'
+    | 'reset-custom-theme'
     | 'settings-changed'
     | 'unlink-plugin'
     | 'beforereload'
@@ -1133,19 +1139,24 @@ class LSPluginCore
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
-  private readonly _userPreferences: Partial<UserPreferences> = {}
-  private readonly _registeredThemes = new Map<
-    PluginLocalIdentity,
-    ThemeOptions[]
-  >()
+  private readonly _userPreferences: UserPreferences = {
+    theme: null,
+    themes: {
+      mode: 'light',
+      light: null,
+      dark: null,
+    },
+    externals: [],
+  }
+  private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
   private readonly _registeredPlugins = new Map<
     PluginLocalIdentity,
     PluginLocal
   >()
   private _currentTheme: {
-    dis: () => void
     pid: PluginLocalIdentity
-    opt: ThemeOptions
+    opt: Theme | LegacyTheme
+    eject: () => void
   }
 
   /**
@@ -1182,12 +1193,25 @@ class LSPluginCore
     }
   }
 
+  /**
+   * Activate the user preferences.
+   *
+   * Steps:
+   *
+   * 1. Load the custom theme.
+   *
+   * @memberof LSPluginCore
+   */
   async activateUserPreferences() {
-    const { theme } = this._userPreferences
-
-    // 0. theme
-    if (theme) {
-      await this.selectTheme(theme, false)
+    const { theme: legacyTheme, themes } = this._userPreferences
+    const currentTheme = themes[themes.mode]
+
+    // If there is currently a theme that has been set
+    if (currentTheme) {
+      await this.selectTheme(currentTheme, { effect: false })
+    } else if (legacyTheme) {
+      // Otherwise compatible with older versions
+      await this.selectTheme(legacyTheme, { effect: false })
     }
   }
 
@@ -1238,7 +1262,7 @@ class LSPluginCore
 
       await this.loadUserPreferences()
 
-      const externals = new Set(this._userPreferences.externals || [])
+      const externals = new Set(this._userPreferences.externals)
 
       if (initial) {
         plugins = plugins.concat(
@@ -1349,8 +1373,8 @@ class LSPluginCore
       this.emit('unregistered', identity)
     }
 
-    const externals = this._userPreferences.externals || []
-    if (externals.length > 0 && unregisteredExternals.length > 0) {
+    const externals = this._userPreferences.externals
+    if (externals.length && unregisteredExternals.length) {
       await this.saveUserPreferences({
         externals: externals.filter((it) => {
           return !unregisteredExternals.includes(it)
@@ -1472,18 +1496,15 @@ class LSPluginCore
     return this._isRegistering
   }
 
-  get themes(): Map<PluginLocalIdentity, ThemeOptions[]> {
+  get themes() {
     return this._registeredThemes
   }
 
-  async registerTheme(
-    id: PluginLocalIdentity,
-    opt: ThemeOptions
-  ): Promise<void> {
-    debug('registered Theme #', id, opt)
+  async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
+    debug('Register theme #', id, opt)
 
     if (!id) return
-    let themes: ThemeOptions[] = this._registeredThemes.get(id)!
+    let themes: Theme[] = this._registeredThemes.get(id)!
     if (!themes) {
       this._registeredThemes.set(id, (themes = []))
     }
@@ -1492,41 +1513,81 @@ class LSPluginCore
     this.emit('theme-changed', this.themes, { id, ...opt })
   }
 
-  async selectTheme(opt?: ThemeOptions, effect = true): Promise<void> {
-    // clear current
+  async selectTheme(
+    theme: Theme | LegacyTheme,
+    options: {
+      effect?: boolean
+      emit?: boolean
+    } = {}
+  ) {
+    const { effect, emit } = Object.assign(
+      {},
+      { effect: true, emit: true },
+      options
+    )
+
+    // Clear current theme before injecting.
     if (this._currentTheme) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
     }
 
-    const disInjectedTheme = setupInjectedTheme(opt?.url)
-    this.emit('theme-selected', opt)
-    effect && (await this.saveUserPreferences({ theme: opt?.url ? opt : null }))
-    if (opt?.url) {
+    // Detect if it is the default theme (no url).
+    if (!theme.url) {
+      this._currentTheme = null
+    } else {
+      const ejectTheme = injectTheme(theme.url)
+
       this._currentTheme = {
-        dis: () => {
-          disInjectedTheme()
-          effect && this.saveUserPreferences({ theme: null })
-        },
-        opt,
-        pid: opt.pid,
+        pid: theme.pid,
+        opt: theme,
+        eject: ejectTheme,
       }
     }
+
+    if (effect) {
+      await this.saveUserPreferences(
+        theme.mode
+          ? {
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
+          : { theme: theme }
+      )
+    }
+
+    if (emit) {
+      this.emit('theme-selected', theme)
+    }
   }
 
-  async unregisterTheme(
-    id: PluginLocalIdentity,
-    effect: boolean = true
-  ): Promise<void> {
-    debug('unregistered Theme #', id)
+  async unregisterTheme(id: PluginLocalIdentity, effect = true) {
+    debug('Unregister theme #', id)
+
+    if (!this._registeredThemes.has(id)) {
+      return
+    }
 
-    if (!this._registeredThemes.has(id)) return
     this._registeredThemes.delete(id)
     this.emit('theme-changed', this.themes, { id })
     if (effect && this._currentTheme?.pid === id) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
       this._currentTheme = null
-      // reset current theme
-      this.emit('theme-selected', null)
+
+      const { theme, themes } = this._userPreferences
+      await this.saveUserPreferences({
+        theme: theme?.pid === id ? null : theme,
+        themes: {
+          ...themes,
+          light: themes.light?.pid === id ? null : themes.light,
+          dark: themes.dark?.pid === id ? null : themes.dark,
+        },
+      })
+
+      // Reset current theme if it is unregistered
+      this.emit('reset-custom-theme', this._userPreferences.themes)
     }
   }
 }

+ 34 - 19
libs/src/LSPlugin.ts

@@ -1,18 +1,24 @@
-import EventEmitter from 'eventemitter3'
 import * as CSS from 'csstype'
+
+import EventEmitter from 'eventemitter3'
 import { LSPluginCaller } from './LSPlugin.caller'
-import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 export type PluginLocalIdentity = string
 
-export type ThemeOptions = {
+export type ThemeMode = 'light' | 'dark'
+
+export interface LegacyTheme {
   name: string
   url: string
   description?: string
-  mode?: 'dark' | 'light'
+  mode?: ThemeMode
+  pid: PluginLocalIdentity
+}
 
-  [key: string]: any
+export interface Theme extends LegacyTheme {
+  mode: ThemeMode
 }
 
 export type StyleString = string
@@ -64,7 +70,7 @@ export interface LSPluginPkgConfig {
   entry: string // alias of main
   title: string
   mode: 'shadow' | 'iframe'
-  themes: Array<ThemeOptions>
+  themes: Theme[]
   icon: string
 
   [key: string]: any
@@ -122,7 +128,7 @@ export interface AppInfo {
  * User's app configurations
  */
 export interface AppUserConfigs {
-  preferredThemeMode: 'dark' | 'light'
+  preferredThemeMode: ThemeMode
   preferredFormat: 'markdown' | 'org'
   preferredDateFormat: string
   preferredStartOfWeek: string
@@ -382,7 +388,7 @@ export interface IAppProxy {
     content: string,
     status?: 'success' | 'warning' | 'error' | string
   ) => void
-  
+
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
@@ -614,9 +620,17 @@ export interface IEditorProxy extends Record<string, any> {
 
   getAllPages: (repo?: string) => Promise<any>
 
-  prependBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  prependBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
-  appendBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  appendBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
   getPreviousSiblingBlock: (
     srcBlock: BlockIdentity
@@ -756,9 +770,7 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(
-    exts: string | string[]
-  ): Promise<{
+  listFilesOfCurrentGraph(exts: string | string[]): Promise<{
     path: string
     size: number
     accessTime: number
@@ -768,14 +780,17 @@ export interface IAssetsProxy {
   }>
 }
 
-export interface ILSPluginThemeManager extends EventEmitter {
-  themes: Map<PluginLocalIdentity, Array<ThemeOptions>>
+export interface ILSPluginThemeManager {
+  get themes(): Map<PluginLocalIdentity, Theme[]>
 
-  registerTheme(id: PluginLocalIdentity, opt: ThemeOptions): Promise<void>
+  registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void>
 
-  unregisterTheme(id: PluginLocalIdentity): Promise<void>
+  unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise<void>
 
-  selectTheme(opt?: ThemeOptions): Promise<void>
+  selectTheme(
+    opt: Theme | LegacyTheme,
+    options: { effect?: boolean; emit?: boolean }
+  ): Promise<void>
 }
 
 export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'
@@ -837,7 +852,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   /**
    * Set the theme for the main Logseq app
    */
-  provideTheme(theme: ThemeOptions): this
+  provideTheme(theme: Theme): this
 
   /**
    * Inject custom css for the main Logseq app

+ 4 - 3
libs/src/LSPlugin.user.ts

@@ -15,7 +15,7 @@ import {
   SlashCommandAction,
   BlockCommandCallback,
   StyleString,
-  ThemeOptions,
+  Theme,
   UIOptions,
   IHookEvent,
   BlockIdentity,
@@ -318,7 +318,8 @@ const KEY_MAIN_UI = 0
  */
 export class LSPluginUser
   extends EventEmitter<LSPluginUserEvents>
-  implements ILSPluginUser {
+  implements ILSPluginUser
+{
   // @ts-ignore
   private _version: string = LIB_VERSION
   private _debugTag: string = ''
@@ -436,7 +437,7 @@ export class LSPluginUser
     return this
   }
 
-  provideTheme(theme: ThemeOptions) {
+  provideTheme(theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }

+ 5 - 10
libs/src/helpers.ts

@@ -416,26 +416,21 @@ export function transformableEvent(target: HTMLElement, e: Event) {
   return obj
 }
 
-let injectedThemeEffect: any = null
-
-export function setupInjectedTheme(url?: string) {
-  injectedThemeEffect?.call()
-
-  if (!url) return
-
+export function injectTheme(url: string) {
   const link = document.createElement('link')
   link.rel = 'stylesheet'
   link.href = url
   document.head.appendChild(link)
 
-  return (injectedThemeEffect = () => {
+  const ejectTheme = () => {
     try {
       document.head.removeChild(link)
     } catch (e) {
       console.error(e)
     }
-    injectedThemeEffect = null
-  })
+  }
+
+  return ejectTheme
 }
 
 export function mergeSettingsWithSchema(

+ 3 - 2
package.json

@@ -5,7 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@capacitor/cli": "3.2.2",
-        "@logseq/nbb-logseq": "^0.3.10",
+        "@logseq/nbb-logseq": "^0.5.103",
         "@playwright/test": "^1.19.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -53,7 +53,7 @@
         "cljs:app-watch": "clojure -M:cljs watch app",
         "cljs:electron-watch": "clojure -M:cljs watch app electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:release": "clojure -M:cljs release app publishing electron",
-        "cljs:release-electron": "clojure -M:cljs release app publishing electron --debug",
+        "cljs:release-electron": "clojure -M:cljs release app electron --debug && clojure -M:cljs release publishing",
         "cljs:release-app": "clojure -M:cljs release app",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",
@@ -73,6 +73,7 @@
         "@capacitor/haptics": "^1.1.4",
         "@capacitor/ios": "3.2.2",
         "@capacitor/keyboard": "^1.2.0",
+        "@capacitor/share": "^1.1.2",
         "@capacitor/splash-screen": "1.1.3",
         "@capacitor/status-bar": "1.0.6",
         "@excalidraw/excalidraw": "0.10.0",

+ 5 - 0
resources/css/common.css

@@ -715,6 +715,11 @@ li p:last-child,
   background-color: var(--ls-primary-background-color, #fff);
 }
 
+.bg-base-4 {
+  background-color: var(--ls-tertiary-background-color);
+}
+
+
 .pre-white-space {
   white-space: pre;
 }

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 3 - 3
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.6.8",
+  "version": "0.6.10",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -36,8 +36,8 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.11",
-    "electron-deeplink": "1.0.9"
+    "@logseq/rsapi": "0.0.14",
+    "electron-deeplink": "1.0.10"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",

+ 17 - 0
scripts/src/logseq/tasks/dev.clj

@@ -24,3 +24,20 @@
                (shell "yarn dev-electron-app")
                (println "Waiting for app to build..."))
              (Thread/sleep 1000))))
+
+
+(defn lint
+  "Run all lint tasks
+  - clj-kondo lint
+  - carve lint for unused vars
+  - lint for vars that are too large
+  - lint invalid translation entries
+  - Lint datalog rules"
+  []
+  (doseq [cmd ["clojure -M:clj-kondo --parallel --lint src"
+               "scripts/carve.clj"
+               "scripts/large_vars.clj"
+               "bb lang:invalid-translations"
+               "scripts/lint_rules.clj"]]
+    (println cmd)
+    (shell cmd)))

+ 6 - 3
shadow-cljs.edn

@@ -29,7 +29,8 @@
                            :source-map         true
                            :externs            ["datascript/externs.js"
                                                 "externs.js"]
-                           :warnings           {:fn-deprecated false}}
+                           :warnings           {:fn-deprecated false
+                                                :redef false}}
         :closure-defines  {goog.debug.LOGGING_ENABLED      true}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
@@ -54,7 +55,8 @@
 
                                 :externs  ["datascript/externs.js"
                                            "externs.js"]
-                                :warnings {:fn-deprecated false}}}
+                                :warnings {:fn-deprecated false
+                                           :redef false}}}
 
   :test {:target          :node-test
          :output-to       "static/tests.js"
@@ -86,7 +88,8 @@
                                   :output-feature-set :es-next
                                   :externs            ["datascript/externs.js"
                                                        "externs.js"]
-                                  :warnings           {:fn-deprecated false}}
+                                  :warnings           {:fn-deprecated false
+                                                       :redef false}}
                :devtools         {:before-load frontend.core/stop
                                   :after-load  frontend.core/start
                                   :preloads    [devtools.preload]}}

+ 51 - 0
src/electron/electron/backup_file.cljs

@@ -0,0 +1,51 @@
+(ns electron.backup-file
+  (:require [clojure.string :as string]
+            ["path" :as path]
+            ["fs" :as fs]
+            ["fs-extra" :as fs-extra]))
+
+(def backup-dir "logseq/bak")
+(def version-file-dir "version-files/local")
+
+(defn- get-backup-dir*
+  [repo relative-path bak-dir]
+  (let [relative-path* (string/replace relative-path repo "")
+        bak-dir (path/join repo bak-dir)
+        path (path/join bak-dir relative-path*)
+        parsed-path (path/parse path)]
+    (path/join (.-dir parsed-path)
+               (.-name parsed-path))))
+
+(defn get-backup-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path backup-dir))
+
+(defn get-version-file-dir
+  [repo relative-path]
+  (get-backup-dir* repo relative-path version-file-dir))
+
+(defn- truncate-old-versioned-files!
+  "reserve the latest 3 version files"
+  [dir]
+  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
+        files (mapv #(.-name %) files)
+        old-versioned-files (drop 3 (reverse (sort files)))]
+    (doseq [file old-versioned-files]
+      (fs-extra/removeSync (path/join dir file)))))
+
+(defn backup-file
+  "backup CONTENT under DIR :backup-dir or :version-file-dir
+  :backup-dir = `backup-dir`
+  :version-file-dir = `version-file-dir`"
+  [repo dir relative-path ext content]
+  {:pre [(contains? #{:backup-dir :version-file-dir} dir)]}
+  (let [dir* (case dir
+               :backup-dir (get-backup-dir repo relative-path)
+               :version-file-dir (get-version-file-dir repo relative-path))
+        new-path (path/join dir*
+                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
+                                 ext))]
+    (fs-extra/ensureDirSync dir*)
+    (fs/writeFileSync new-path content)
+    (fs/statSync new-path)
+    (truncate-old-versioned-files! dir*)))

+ 11 - 9
src/electron/electron/fs_watcher.cljs

@@ -4,7 +4,6 @@
             ["chokidar" :as watcher]
             [electron.utils :as utils]
             ["electron" :refer [app]]
-            [frontend.util.fs :as util-fs]
             [electron.window :as window]))
 
 ;; TODO: explore different solutions for different platforms
@@ -30,10 +29,15 @@
 
 (defn- publish-file-event!
   [dir path event]
-  (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
-                                 :path (utils/fix-win-path! path)
-                                 :content (utils/read-file path)
-                                 :stat (fs/statSync path)}))
+  (let [content (when (and (not= event "unlink")
+                           (utils/should-read-content? path))
+                  (utils/read-file path))
+        stat (when (not= event "unlink")
+               (fs/statSync path))]
+    (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
+                                   :path (utils/fix-win-path! path)
+                                   :content content
+                                   :stat stat})))
 
 (defn watch-dir!
   "Watch a directory if no such file watcher exists"
@@ -43,7 +47,7 @@
     (let [watcher (.watch watcher dir
                           (clj->js
                            {:ignored (fn [path]
-                                       (util-fs/ignored-path? dir path))
+                                       (utils/ignored-path? dir path))
                             :ignoreInitial false
                             :ignorePermissionErrors true
                             :interval polling-interval
@@ -63,9 +67,7 @@
              (publish-file-event! dir path "change")))
       (.on watcher "unlink"
            (fn [path]
-             (send-file-watcher! dir "unlink"
-                                 {:dir (utils/fix-win-path! dir)
-                                  :path (utils/fix-win-path! path)})))
+             (publish-file-event! dir path "unlink")))
       (.on watcher "error"
            (fn [path]
              (println "Watch error happened: "

+ 8 - 34
src/electron/electron/handler.cljs

@@ -19,7 +19,8 @@
             [electron.git :as git]
             [electron.plugin :as plugin]
             [electron.window :as win]
-            [electron.file-sync-rsapi :as rsapi]))
+            [electron.file-sync-rsapi :as rsapi]
+            [electron.backup-file :as backup-file]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -65,45 +66,18 @@
   (let [result (.diff_main Diff old new)]
     (some (fn [a] (= -1 (first a))) result)))
 
-(defn- truncate-old-versioned-files!
-  [dir]
-  (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))
-        files (map #(.-name %) files)
-        old-versioned-files (drop 3 (reverse (sort files)))]
-    (doseq [file old-versioned-files]
-      (fs-extra/removeSync (path/join dir file)))))
-
-(defn- get-backup-dir
-  [repo path]
-  (let [path (string/replace path repo "")
-        bak-dir (str repo "/logseq/bak")
-        path (str bak-dir path)
-        parsed-path (path/parse path)]
-    (path/join (.-dir parsed-path)
-               (.-name parsed-path))))
-
-(defn backup-file
-  [repo path content]
-  (let [path-dir (get-backup-dir repo path)
-        ext (path/extname path)
-        new-path (path/join path-dir
-                            (str (string/replace (.toISOString (js/Date.)) ":" "_")
-                                 ext))]
-    (fs-extra/ensureDirSync path-dir)
-    (fs/writeFileSync new-path content)
-    (fs/statSync new-path)
-    (truncate-old-versioned-files! path-dir)
-    new-path))
-
 (defmethod handle :backupDbFile [_window [_ repo path db-content new-content]]
   (when (and (string? db-content)
              (string? new-content)
              (string-some-deleted? db-content new-content))
-    (backup-file repo path db-content)))
+    (backup-file/backup-file repo :backup-dir path (path/extname path) db-content)))
+
+(defmethod handle :addVersionFile [_window [_ repo path content]]
+  (backup-file/backup-file repo :version-file-dir path (path/extname path) content))
 
 (defmethod handle :openFileBackupDir [_window [_ repo path]]
   (when (string? path)
-    (let [dir (get-backup-dir repo path)]
+    (let [dir (backup-file/get-backup-dir repo path)]
       (.openPath shell dir))))
 
 (defmethod handle :readFile [_window [_ path]]
@@ -129,7 +103,7 @@
       (fs/statSync path)
       (catch :default e
         (let [backup-path (try
-                            (backup-file repo path content)
+                            (backup-file/backup-file repo :backup-dir path (path/extname path) content)
                             (catch :default e
                               (println "Backup file failed")
                               (js/console.dir e)))]

+ 13 - 13
src/electron/electron/utils.cljs

@@ -62,27 +62,27 @@
   (when-let [agent (cfgs/get-item :settings/agent)]
     (set-fetch-agent agent)))
 
-;; keep same as ignored-path? in src/main/frontend/util/fs.cljs
-;; TODO: merge them
 (defn ignored-path?
+  "Ignore given path from file-watcher notification"
   [dir path]
   (when (string? path)
     (or
      (some #(string/starts-with? path (str dir "/" %))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
      (some #(string/includes? path (str "/" % "/"))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
-     (string/ends-with? path ".DS_Store")
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
+     (some #(string/ends-with? path %)
+           [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
      ;; hidden directory or file
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)
-           (re-find #"^\.[^.]+" relpath)))
-     (let [path (string/lower-case path)]
-       (and
-        (not (string/blank? (path/extname path)))
-        (not
-         (some #(string/ends-with? path %)
-               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))
+           (re-find #"^\.[^.]+" relpath))))))
+
+(defn should-read-content?
+  "Skip reading content of file while using file-watcher"
+  [path]
+  (let [ext (string/lower-case (path/extname path))]
+    (contains? #{".md" ".markdown" ".org" ".js" ".edn" ".css"} ext)))
 
 (defn fix-win-path!
   [path]
@@ -131,4 +131,4 @@
 
 (defn normalize-lc
   [s]
-  (normalize (string/lower-case s)))
+  (normalize (string/lower-case s)))

+ 6 - 12
src/main/frontend/commands.cljs

@@ -281,20 +281,14 @@
                  (p/let [_ (draw/create-draw-with-default-content path)]
                    (println "draw file created, " path))
                  text)) "Draw a graph with Excalidraw"]
-
-     (when (util/zh-CN-supported?)
-       ["Embed Bilibili video" [[:editor/input "{{bilibili }}" {:last-pattern (state/get-editor-command-trigger)
-                                                                :backward-pos 2}]]])
+     
      ["Embed HTML " (->inline "html")]
 
-     ["Embed Youtube video" [[:editor/input "{{youtube }}" {:last-pattern (state/get-editor-command-trigger)
-                                                            :backward-pos 2}]]]
+     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger)
+                                                    :backward-pos 2}]]]
 
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
-     ["Embed Vimeo video" [[:editor/input "{{vimeo }}" {:last-pattern (state/get-editor-command-trigger)
-                                                        :backward-pos 2}]]]
-
      ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger)
                                                           :backward-pos 2}]]]]
 
@@ -517,7 +511,7 @@
 
 (defn compute-pos-delta-when-change-marker
   [edit-content marker pos]
-  (let [old-marker (some->> (first (gp-util/safe-re-find marker/bare-marker-pattern edit-content))
+  (let [old-marker (some->> (first (util/safe-re-find marker/bare-marker-pattern edit-content))
                             (string/trim))
         pos-delta (- (count marker)
                      (count old-marker))
@@ -542,7 +536,7 @@
                   (if-let [matches (seq (util/re-pos new-line-re-pattern prefix))]
                     (let [[start-pos content] (last matches)]
                       (+ start-pos (count content)))
-                    (count (gp-util/safe-re-find re-pattern prefix))))
+                    (count (util/safe-re-find re-pattern prefix))))
             new-value (str (subs edit-content 0 pos)
                            (string/replace-first (subs edit-content pos)
                                                  (marker/marker-pattern format)
@@ -583,7 +577,7 @@
       (let [edit-content (gobj/get current-input "value")
             heading-pattern #"^#+\s+"
             new-value (cond
-                        (gp-util/safe-re-find heading-pattern edit-content)
+                        (util/safe-re-find heading-pattern edit-content)
                         (string/replace-first edit-content
                                               heading-pattern
                                               (str heading " "))

+ 253 - 156
src/main/frontend/components/block.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.block
   (:refer-clojure :exclude [range])
   (:require ["/frontend/utils" :as utils]
+            ["@capacitor/share" :refer [^js Share]]
             [cljs-bean.core :as bean]
             [cljs.core.match :refer [match]]
             [cljs.reader :as reader]
@@ -47,7 +48,7 @@
             [frontend.security :as security]
             [frontend.state :as state]
             [frontend.template :as template]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
@@ -55,6 +56,8 @@
             [frontend.util.property :as property]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.block :as gp-block]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
@@ -261,14 +264,33 @@
   (let [src (::src state)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         href (config/get-local-asset-absolute-path href)]
-    (when (or granted? (util/electron?) (mobile-util/is-native-platform?))
+    (when (or granted? (util/electron?) (mobile-util/native-platform?))
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
-      (let [ext (util/get-file-ext @src)]
-        (if (contains? (set (map name config/audio-formats)) ext)
+      (let [ext (keyword (util/get-file-ext @src))
+            share-fn (fn [event]
+                       (util/stop event)
+                       (when (mobile-util/native-platform?)
+                         (p/let [url (str (config/get-repo-dir (state/get-current-repo)) href)]
+                           (.share Share #js {:url url
+                                              :title "Open file with your favorite app"}))))]
+        (cond
+          (contains? config/audio-formats ext)
           (audio-cp @src)
-          (resizable-image config title @src metadata full_text true))))))
+
+          (contains? (config/img-formats) ext)
+          (resizable-image config title @src metadata full_text true)
+
+          (= ext :pdf)
+          [:a.asset-ref.is-pdf {:href @src
+                                :on-click share-fn}
+           title]
+
+          :else
+          [:a.asset-ref.is-doc {:ref @src
+                                :on-click share-fn}
+           title])))))
 
 (defn ar-url->http-url
   [href]
@@ -436,41 +458,66 @@
 
 (rum/defc page-preview-trigger
   [{:keys [children sidebar? tippy-position tippy-distance fixed-position? open? manual?] :as config} page-name]
-  (let [page-name (util/page-name-sanity-lc page-name)
+  (let [*tippy-ref (rum/create-ref)
+        page-name (util/page-name-sanity-lc page-name)
         redirect-page-name (or (model/get-redirect-page-name page-name (:block/alias? config))
                                page-name)
         page-original-name (model/get-page-original-name redirect-page-name)
-        html-template (fn []
-                        (when redirect-page-name
-                          [:div.tippy-wrapper.overflow-y-auto.p-4
-                           {:style {:width          600
-                                    :text-align     "left"
-                                    :font-weight    500
-                                    :max-height     600
-                                    :padding-bottom 64}}
-                           (if (and (string? page-original-name) (string/includes? page-original-name "/"))
-                             [:div.my-2
-                              (->>
-                               (for [page (string/split page-original-name #"/")]
-                                 (when (and (string? page) page)
-                                   (page-reference false page {} nil)))
-                               (interpose [:span.mx-2.opacity-30 "/"]))]
-                             [:h2.font-bold.text-lg (if (= page-name redirect-page-name)
-                                                      page-original-name
-                                                      [:span
-                                                       [:span.text-sm.mr-2 "Alias:"]
-                                                       page-original-name])])
-                           (let [page (db/entity [:block/name (util/page-name-sanity-lc redirect-page-name)])]
-                             (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name {:redirect? false})
-                             (when-let [f (state/get-page-blocks-cp)]
-                               (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))]))]
+        _  #_:clj-kondo/ignore (rum/defc html-template []
+                        (let [*el-popup (rum/use-ref nil)]
+
+                          (rum/use-effect!
+                            (fn []
+                              (let [el-popup (rum/deref *el-popup)
+                                    cb (fn [^js e]
+                                         (when-not (:editor/editing? @state/state)
+                                           ;; Esc
+                                           (and (= e.which 27)
+                                                (when-let [tp (rum/deref *tippy-ref)]
+                                                  (.hideTooltip tp)))))]
+
+                                (js/setTimeout #(.focus el-popup))
+                                (.addEventListener el-popup "keyup" cb)
+                                #(.removeEventListener el-popup "keyup" cb)))
+                            [])
+
+                          (when redirect-page-name
+                            [:div.tippy-wrapper.overflow-y-auto.p-4.outline-none
+                             {:ref   *el-popup
+                              :tab-index -1
+                              :style {:width          600
+                                      :text-align     "left"
+                                      :font-weight    500
+                                      :max-height     600
+                                      :padding-bottom 64}}
+                             (if (and (string? page-original-name) (string/includes? page-original-name "/"))
+                               [:div.my-2
+                                (->>
+                                  (for [page (string/split page-original-name #"/")]
+                                    (when (and (string? page) page)
+                                      (page-reference false page {} nil)))
+                                  (interpose [:span.mx-2.opacity-30 "/"]))]
+                               [:h2.font-bold.text-lg (if (= page-name redirect-page-name)
+                                                        page-original-name
+                                                        [:span
+                                                         [:span.text-sm.mr-2 "Alias:"]
+                                                         page-original-name])])
+                             (let [page (db/entity [:block/name (util/page-name-sanity-lc redirect-page-name)])]
+                               (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name {:redirect? false})
+                               (when-let [f (state/get-page-blocks-cp)]
+                                 (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))])))]
+
     (if (or (not manual?) open?)
-      (ui/tippy {:html            html-template
+      (ui/tippy {:ref             *tippy-ref
+                 :html            html-template
                  :interactive     true
                  :delay           [1000, 100]
                  :fixed-position? fixed-position?
                  :position        (or tippy-position "top")
-                 :distance        (or tippy-distance 10)}
+                 :distance        (or tippy-distance 10)
+                 :popperOptions   {:modifiers {:preventOverflow
+                                               {:enabled           true
+                                                :boundariesElement "viewport"}}}}
                 children)
       children)))
 
@@ -478,7 +525,7 @@
   "Accepts {:block/name sanitized / unsanitized page-name}"
   [{:keys [html-export? redirect-page-name label children contents-page? preview?] :as config} page]
   (when-let [page-name-in-block (:block/name page)]
-    (let [page-name-in-block (util/remove-boundary-slashes page-name-in-block)
+    (let [page-name-in-block (gp-util/remove-boundary-slashes page-name-in-block)
           page-name (util/page-name-sanity-lc page-name-in-block)
           page-entity (db/entity [:block/name page-name])
           redirect-page-name (or (and (= :org (state/get-preferred-format))
@@ -639,7 +686,7 @@
   (and (= 1 (count label))
        (let [label (first label)]
          (string? (last label))
-         (last label))))
+         (js/decodeURIComponent (last label)))))
 
 (defn- get-page
   [label]
@@ -660,11 +707,8 @@
 (rum/defc block-reference < rum/reactive
   db-mixins/query
   [config id label]
-  (when (and
-         (not (string/blank? id))
-         (gp-util/uuid-string? id))
-    (let [block-id (uuid id)
-          block (db/pull-block block-id)
+  (when-let [block-id (parse-uuid id)]
+    (let [block (db/pull-block block-id)
           block-type (keyword (get-in block [:block/properties :ls-type]))
           hl-type (get-in block [:block/properties :hl-type])
           repo (state/get-current-repo)]
@@ -727,13 +771,13 @@
    (inline-text {} format v))
   ([config format v]
    (when (string? v)
-     (let [inline-list (mldoc/inline->edn v (mldoc/default-config format))]
+     (let [inline-list (gp-mldoc/inline->edn v (gp-mldoc/default-config format))]
        [:div.inline.mr-1 (map-inline config inline-list)]))))
 
 (defn- render-macro
   [config name arguments macro-content format]
   (if macro-content
-    (let [ast (->> (mldoc/->edn macro-content (mldoc/default-config format))
+    (let [ast (->> (mldoc/->edn macro-content (gp-mldoc/default-config format))
                    (map first))
           paragraph? (and (= 1 (count ast))
                           (= "Paragraph" (ffirst ast)))]
@@ -821,23 +865,33 @@
 
 (defn- media-link
   [config url s label metadata full_text]
-  (let [ext (util/get-file-ext s)]
+  (let [ext (keyword (util/get-file-ext s))
+        label-text (get-label-text label)]
     (cond
-      (contains? (set (map name config/audio-formats)) ext)
+      (contains? config/audio-formats ext)
       (audio-link config url s label metadata full_text)
 
-      (not (contains? #{"pdf" "mp4" "webm" "mov"} ext))
-      (image-link config url s label metadata full_text)
-
-      (util/electron?)
-      (if (= (util/get-file-ext s) "pdf")
+      (= ext :pdf)
+      (cond
+        (util/electron?)
         [:a.asset-ref.is-pdf
          {:href "javascript:void(0);"
           :on-mouse-down (fn [_event]
                            (when-let [current (pdf-assets/inflate-asset s)]
                              (state/set-state! :pdf/current current)))}
-         (get-label-text label)]
-        (asset-reference config label s)))))
+         label-text]
+
+        (mobile-util/native-platform?)
+        (asset-link config label-text s metadata full_text))
+
+      (contains? (config/doc-formats) ext)
+      (asset-link config label-text s metadata full_text)
+
+      (not (contains? #{:mp4 :webm :mov} ext))
+      (image-link config url s label metadata full_text)
+
+      :else
+      (asset-reference config label s))))
 
 (defn- search-link-cp
   [config url s label title metadata full_text]
@@ -860,7 +914,7 @@
     (not (string/includes? s "."))
     (page-reference (:html-export? config) s config label)
 
-    (util/url? s)
+    (gp-util/url? s)
     (->elem :a {:href s
                 :data-href s
                 :target "_blank"}
@@ -933,7 +987,7 @@
                (= "Complex" protocol)
                (= (string/lower-case (:protocol path)) "id")
                (string? (:link path))
-               (gp-util/uuid-string? (:link path))) ; org mode id
+               (util/uuid-string? (:link path))) ; org mode id
           (let [id (uuid (:link path))
                 block (db/entity [:block/uuid id])]
             (if (:block/pre-block? block)
@@ -953,7 +1007,7 @@
                   show-brackets? (state/show-brackets?)]
               (if (and page
                        (when-let [ext (util/get-file-ext href)]
-                         (config/mldoc-support? ext)))
+                         (gp-config/mldoc-support? ext)))
                 [:span.page-reference
                  (when show-brackets? [:span.text-gray-500 "[["])
                  (page-cp config page)
@@ -1047,10 +1101,7 @@
       (when-let [s (-> (string/replace a "((" "")
                        (string/replace "))" "")
                        string/trim)]
-        (when-let [id (and s
-                           (let [s (string/trim s)]
-                             (and (gp-util/uuid-string? s)
-                                  (uuid s))))]
+        (when-let [id (some-> s string/trim parse-uuid)]
           (block-embed (assoc config :link-depth (inc link-depth)) id)))
 
       :else                         ;TODO: maybe collections?
@@ -1059,42 +1110,80 @@
 (defn- macro-vimeo-cp
   [_config arguments]
   (when-let [url (first arguments)]
-    (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"]
-      (when-let [vimeo-id (nth (gp-util/safe-re-find Vimeo-regex url) 5)]
-        (when-not (string/blank? vimeo-id)
-          (let [width (min (- (util/get-width) 96)
-                           560)
-                height (int (* width (/ 315 560)))]
-            [:iframe
-             {:allow-full-screen "allowfullscreen"
-              :allow
-              "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-              :frame-border "0"
-              :src (str "https://player.vimeo.com/video/" vimeo-id)
-              :height height
-              :width width}]))))))
+    (when-let [vimeo-id (nth (util/safe-re-find text/vimeo-regex url) 5)]
+      (when-not (string/blank? vimeo-id)
+        (let [width (min (- (util/get-width) 96)
+                         560)
+              height (int (* width (/ 315 560)))]
+          [:iframe
+           {:allow-full-screen "allowfullscreen"
+            :allow
+            "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+            :frame-border "0"
+            :src (str "https://player.vimeo.com/video/" vimeo-id)
+            :height height
+            :width width}])))))
 
 (defn- macro-bilibili-cp
   [_config arguments]
   (when-let [url (first arguments)]
-    (let [id-regex #"https?://www\.bilibili\.com/video/([^? ]+)"]
-      (when-let [id (cond
-                      (<= (count url) 15) url
-                      :else
-                      (last (gp-util/safe-re-find id-regex url)))]
-        (when-not (string/blank? id)
-          (let [width (min (- (util/get-width) 96)
-                           560)
-                height (int (* width (/ 315 560)))]
-            [:iframe
-             {:allowfullscreen true
-              :framespacing "0"
-              :frameborder "no"
-              :border "0"
-              :scrolling "no"
-              :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
-              :width width
-              :height (max 500 height)}]))))))
+    (when-let [id (cond
+                    (<= (count url) 15) url
+                    :else
+                    (nth (util/safe-re-find text/bilibili-regex url) 5))]
+      (when-not (string/blank? id)
+        (let [width (min (- (util/get-width) 96)
+                         560)
+              height (int (* width (/ 315 560)))]
+          [:iframe
+           {:allowfullscreen true
+            :framespacing "0"
+            :frameborder "no"
+            :border "0"
+            :scrolling "no"
+            :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+            :width width
+            :height (max 500 height)}])))))
+
+(defn- macro-video-cp
+  [_config arguments]
+  (when-let [url (first arguments)]
+    (let [width (min (- (util/get-width) 96)
+                     560)
+          height (int (* width (/ 315 560)))
+          results (text/get-matched-video url)
+          src (match results
+                     [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
+                     (if (= (count id) 11) ["youtube-player" id] url)
+
+                     [_ _ _ "youtube-nocookie.com" _ id _]
+                     (str "https://www.youtube-nocookie.com/embed/" id)
+
+                     [_ _ _ "loom.com" _ id _]
+                     (str "https://www.loom.com/embed/" id)
+
+                     [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
+                     (str "https://player.vimeo.com/video/" id)
+
+                     [_ _ _ "bilibili.com" _ id _]
+                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+
+                     :else
+                     url)]
+      (if (and (coll? src)
+               (= (first src) "youtube-player"))
+        (youtube/youtube-video (last src))
+        (when src
+          [:iframe
+           {:allowfullscreen true
+            :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+            :framespacing "0"
+            :frameborder "no"
+            :border "0"
+            :scrolling "no"
+            :src src
+            :width width
+            :height height}])))))
 
 (defn- macro-else-cp
   [name config arguments]
@@ -1207,13 +1296,12 @@
 
       (= name "youtube")
       (when-let [url (first arguments)]
-        (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"]
-          (when-let [youtube-id (cond
-                                  (== 11 (count url)) url
-                                  :else
-                                  (nth (gp-util/safe-re-find YouTube-regex url) 5))]
-            (when-not (string/blank? youtube-id)
-              (youtube/youtube-video youtube-id)))))
+        (when-let [youtube-id (cond
+                                (== 11 (count url)) url
+                                :else
+                                (nth (util/safe-re-find text/youtube-regex url) 5))]
+          (when-not (string/blank? youtube-id)
+            (youtube/youtube-video youtube-id))))
 
       (= name "youtube-timestamp")
       (when-let [timestamp (first arguments)]
@@ -1236,13 +1324,16 @@
       (= name "bilibili")
       (macro-bilibili-cp config arguments)
 
+      (= name "video")
+      (macro-video-cp config arguments)
+      
       (contains? #{"tweet" "twitter"} name)
       (when-let [url (first arguments)]
         (let [id-regex #"/status/(\d+)"]
           (when-let [id (cond
                           (<= (count url) 15) url
                           :else
-                          (last (gp-util/safe-re-find id-regex url)))]
+                          (last (util/safe-re-find id-regex url)))]
             (ui/tweet-embed id))))
 
       (= name "embed")
@@ -1280,7 +1371,7 @@
          (->elem :sub (map-inline config l))
 
          ["Tag" _]
-         (when-let [s (block/get-tag item)]
+         (when-let [s (gp-block/get-tag item)]
            (let [s (text/page-ref-un-brackets! s)]
              (page-cp (assoc config :tag? true) {:block/name s})))
 
@@ -1481,7 +1572,7 @@
                                    "hide-inner-bullet"))}
                     [:span.bullet {:blockid (str uuid)}]]]]
        (cond
-         (and (or (mobile-util/is-native-platform?)
+         (and (or (mobile-util/native-platform?)
                   (:ui/show-empty-bullets? (state/get-config)))
               (not doc-mode?))
          bullet
@@ -1711,8 +1802,8 @@
          (for [elem elems]
            (rum/with-key elem (str (random-uuid)))))
 
-       (and (string? v) (util/wrapped-by-quotes? v))
-       (util/unquote-string v)
+       (and (string? v) (gp-util/wrapped-by-quotes? v))
+       (gp-util/unquote-string v)
 
        :else
        (inline-text config (:block/format block) (str v)))]))
@@ -1815,35 +1906,40 @@
   (.stopPropagation e)
   (let [target (gobj/get e "target")
         button (gobj/get e "buttons")
-        shift? (gobj/get e "shiftKey")]
-    (when (contains? #{1 0} button)
-      (when-not (target-forbidden-edit? target)
-        (if (and shift? (state/get-selection-start-block))
-          (editor-handler/highlight-selection-area! block-id)
-          (do
-            (editor-handler/clear-selection!)
-            (editor-handler/unhighlight-blocks!)
-            (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block)
-                           cursor-range (util/caret-range (gdom/getElement block-id))
-                           {:block/keys [content format]} block
-                           content (->> content
-                                        (property/remove-built-in-properties format)
-                                        (drawer/remove-logbook))]
-                       ;; save current editing block
-                       (let [{:keys [value] :as state} (editor-handler/get-state)]
-                         (editor-handler/save-block! state value))
-                       (state/set-editing!
-                        edit-input-id
-                        content
-                        block
-                        cursor-range
-                        false))]
-              ;; wait a while for the value of the caret range
-              (if (util/ios?)
-                (f)
-                (js/setTimeout f 5))
-
-              (when block-id (state/set-selection-start-block! block-id)))))))))
+        shift? (gobj/get e "shiftKey")
+        meta? (util/meta-key? e)]
+    (if (and meta? (not (state/get-edit-input-id)))
+      (do
+        (util/stop e)
+        (state/conj-selection-block! (gdom/getElement block-id) :down))
+      (when (contains? #{1 0} button)
+        (when-not (target-forbidden-edit? target)
+          (if (and shift? (state/get-selection-start-block))
+            (editor-handler/highlight-selection-area! block-id)
+            (do
+              (editor-handler/clear-selection!)
+              (editor-handler/unhighlight-blocks!)
+              (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block)
+                             cursor-range (util/caret-range (gdom/getElement block-id))
+                             {:block/keys [content format]} block
+                             content (->> content
+                                          (property/remove-built-in-properties format)
+                                          (drawer/remove-logbook))]
+                         ;; save current editing block
+                         (let [{:keys [value] :as state} (editor-handler/get-state)]
+                           (editor-handler/save-block! state value))
+                         (state/set-editing!
+                          edit-input-id
+                          content
+                          block
+                          cursor-range
+                          false))]
+                ;; wait a while for the value of the caret range
+                (if (util/ios?)
+                  (f)
+                  (js/setTimeout f 5))
+
+                (when block-id (state/set-selection-start-block! block-id))))))))))
 
 (rum/defc dnd-separator-wrapper < rum/reactive
   [block block-id slide? top? block-content?]
@@ -1916,7 +2012,8 @@
                              (when (and
                                     (state/in-selection-mode?)
                                     (not (string/includes? content "```"))
-                                    (not (gobj/get e "shiftKey")))
+                                    (not (gobj/get e "shiftKey"))
+                                    (not (util/meta-key? e)))
                                ;; clear highlighted text
                                (util/clear-selection!)))}
        (not slide?)
@@ -2188,20 +2285,21 @@
 
 (defn- block-mouse-over
   [uuid e *control-show? block-id doc-mode?]
-  (util/stop e)
-  (when (or
-         (model/block-collapsed? uuid)
-         (editor-handler/collapsable? uuid {:semantic? true}))
-    (reset! *control-show? true))
-  (when-let [parent (gdom/getElement block-id)]
-    (let [node (.querySelector parent ".bullet-container")]
-      (when doc-mode?
-        (dom/remove-class! node "hide-inner-bullet"))))
-  (when (and
-         (state/in-selection-mode?)
-         (non-dragging? e))
+  (when-not @*dragging?
     (util/stop e)
-    (editor-handler/highlight-selection-area! block-id)))
+    (when (or
+           (model/block-collapsed? uuid)
+           (editor-handler/collapsable? uuid {:semantic? true}))
+      (reset! *control-show? true))
+    (when-let [parent (gdom/getElement block-id)]
+      (let [node (.querySelector parent ".bullet-container")]
+        (when doc-mode?
+          (dom/remove-class! node "hide-inner-bullet"))))
+    (when (and
+           (state/in-selection-mode?)
+           (non-dragging? e))
+      (util/stop e)
+      (editor-handler/highlight-selection-area! block-id))))
 
 (defn- block-mouse-leave
   [e *control-show? block-id doc-mode?]
@@ -2404,14 +2502,13 @@
   [state config block]
   (let [repo (state/get-current-repo)
         ref? (:ref? config)
-        custom-query? (boolean (:custom-query? config))
-        ref-or-custom-query? (or ref? custom-query?)]
-    (if (and ref-or-custom-query? (not (:ref-query-child? config)))
+        custom-query? (boolean (:custom-query? config))]
+    (if (and ref? (not custom-query?) (not (:ref-query-child? config)))
       (ui/lazy-visible
-       nil
        (fn []
          (block-container-inner state repo config block))
-       nil)
+       nil
+       {:reset-height? false})
       (block-container-inner state repo config block))))
 
 (defn divide-lists
@@ -2739,9 +2836,9 @@
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
    (ui/lazy-visible
-    "loading ..."
     (fn [] (custom-query* config q))
-    nil)))
+    nil
+    {:reset-height? true})))
 
 (defn admonition
   [config type result]
@@ -2767,11 +2864,11 @@
 ;;     (cond
 ;;       (= lang "quote")
 ;;       (let [content (string/trim (string/join "\n" lines))]
-;;         ["Quote" (first (mldoc/->edn content (mldoc/default-config :markdown)))])
+;;         ["Quote" (first (mldoc/->edn content (gp-mldoc/default-config :markdown)))])
 
 ;;       (contains? #{"query" "note" "tip" "important" "caution" "warning" "pinned"} lang)
 ;;       (let [content (string/trim (string/join "\n" lines))]
-;;         ["Custom" lang nil (first (mldoc/->edn content (mldoc/default-config :markdown))) content])
+;;         ["Custom" lang nil (first (mldoc/->edn content (gp-mldoc/default-config :markdown))) content])
 
 ;;       :else
 ;;       ["Src" options])))
@@ -2790,7 +2887,7 @@
         :else
         (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
           (if (:slide? config)
-            (highlight/highlight (str (medley/random-uuid))
+            (highlight/highlight (str (random-uuid))
                                  {:class (str "language-" language)
                                   :data-lang language}
                                  code)
@@ -2859,7 +2956,7 @@
 
         ["Paragraph" l]
              ;; TODO: speedup
-        (if (gp-util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l))
+        (if (util/safe-re-find #"\"Export_Snippet\" \"embed\"" (str l))
           (->elem :div (map-inline config l))
           (->elem :div.is-paragraph (map-inline config l)))
 
@@ -3038,7 +3135,7 @@
                              (when (> (- (util/time-ms) (:start-time config)) 100)
                                (load-more-blocks! config flat-blocks)))
             has-more? (and
-                       (> (count flat-blocks) model/initial-blocks-length)
+                       (>= (count flat-blocks) model/initial-blocks-length)
                        (some? (model/get-next-open-block (db/get-db) (last flat-blocks) db-id)))
             dom-id (str "lazy-blocks-" (::id state))]
         [:div {:id dom-id}

+ 14 - 9
src/main/frontend/components/block.css

@@ -4,7 +4,7 @@
 
   @screen sm {
     width: calc(100% - 33px);
-    overflow-x: auto;
+    overflow-x: visible;
   }
 }
 
@@ -223,14 +223,6 @@
     }
 }
 
-html.is-mobile,
-html.is-native-iphone,
-html.is-native-android {
-  .references .block-control {
-    margin-left: -20px;
-  }
-}
-
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -278,6 +270,19 @@ html.is-native-android {
       align-items: center;
     }
   }
+
+  &.is-doc {
+      &:before {
+          content: "[[📜";
+          opacity: .7;
+          margin-right: 4px;
+      }
+
+      &:after {
+          content: "]]";
+          opacity: .7;
+      }
+  }
 }
 
 .embed-page {

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

@@ -36,7 +36,7 @@
 
 (defn- lazy-load
   [format]
-  (let [format (format/normalize format)]
+  (let [format (gp-util/normalize-format format)]
     (when-let [record (format/get-format-record format)]
       (when-not (protocol/loaded? record)
         (set-format-js-loading! format true)
@@ -363,13 +363,13 @@
                            e
                            (custom-context-menu-content))
 
-                          (and block-id (gp-util/uuid-string? block-id))
+                          (and block-id (parse-uuid block-id))
                           (let [block (.closest target ".ls-block")]
                             (when block
                               (util/select-highlight! [block]))
                             (common-handler/show-custom-context-menu!
                             e
-                            (block-context-menu-content target (cljs.core/uuid block-id))))
+                            (block-context-menu-content target (uuid block-id))))
 
                           :else
                           nil))))))
@@ -388,7 +388,7 @@
                    :format format}
                   id
                   config)
-      (let [format (format/normalize format)
+      (let [format (gp-util/normalize-format format)
             loading? (get loading format)
             markup? (contains? config/html-render-formats format)
             on-click (fn [e]
@@ -446,5 +446,5 @@
   (if hiccup
     [:div
      (hiccup-content id option)]
-    (let [format (format/normalize format)]
+    (let [format (gp-util/normalize-format format)]
       (non-hiccup-content id content on-click on-hide config format))))

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

@@ -7,9 +7,6 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
-            [frontend.mobile.camera :as mobile-camera]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.extensions.zotero :as zotero]
@@ -27,9 +24,7 @@
             [goog.dom :as gdom]
             [promesa.core :as p]
             [rum.core :as rum]
-            [frontend.handler.history :as history]
-            [frontend.mobile.footer :as footer]
-            [frontend.handler.config :as config-handler]))
+            [frontend.mobile.footer :as footer]))
 
 (rum/defc commands < rum/reactive
   [id format]
@@ -232,89 +227,6 @@
                            template)
             :class       "black"}))))))
 
-(rum/defc mobile-bar-indent-outdent [indent? icon]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (editor-handler/indent-outdent indent?))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(def ^:private mobile-bar-icons-keywords
-  [:checkbox :brackets :parentheses :command :tag :a-b :list :camera
-   :brand-youtube :link :rotate :rotate-clockwise :code :bold :italic :strikethrough :paint])
-
-(def ^:private mobile-bar-commands-stats
-  (atom (into {}
-              (mapv (fn [name] [name {:counts 0}])
-                    mobile-bar-icons-keywords))))
-
-(defn set-command-stats [icon]
-  (let [key (keyword icon)
-        counts (get-in @mobile-bar-commands-stats [key :counts])]
-    (swap! mobile-bar-commands-stats
-           assoc-in [key :counts] (inc counts))
-    (config-handler/set-config!
-     :mobile/toolbar-stats @mobile-bar-commands-stats)))
-
-(rum/defc mobile-bar-command
-  [command-handler icon & [count? event?]]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (when count?
-                        (set-command-stats icon))
-                      (if event?
-                        (command-handler e)
-                        (command-handler)))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(defn mobile-bar-commands
-  [_parent-state parent-id]
-  (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)]
-                             (util/make-el-cursor-position-into-center-viewport input)
-                             (.focus input)))]
-    (zipmap mobile-bar-icons-keywords
-     [(mobile-bar-command editor-handler/cycle-todo! "checkbox" true)
-      (mobile-bar-command #(editor-handler/toggle-page-reference-embed parent-id) "brackets" true)
-      (mobile-bar-command #(editor-handler/toggle-block-reference-embed parent-id) "parentheses" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true)
-      (mobile-bar-command editor-handler/cycle-priority! "a-b" true)
-      (mobile-bar-command editor-handler/toggle-list! "list" true)
-      (mobile-bar-command #(mobile-camera/embed-photo parent-id) "camera" true)
-      (mobile-bar-command commands/insert-youtube-timestamp "brand-youtube" true)
-      (mobile-bar-command editor-handler/html-link-format! "link" true)
-      (mobile-bar-command history/undo! "rotate" true true)
-      (mobile-bar-command history/redo! "rotate-clockwise" true true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "<" {})) "code" true)
-      (mobile-bar-command editor-handler/bold-format! "bold" true)
-      (mobile-bar-command editor-handler/italics-format! "italic" true)
-      (mobile-bar-command editor-handler/strike-through-format! "strikethrough" true)
-      (mobile-bar-command editor-handler/highlight-format! "paint" true)])))
-
-(rum/defc mobile-bar < rum/reactive
-  [parent-state parent-id]
-  (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))]
-   (reset! mobile-bar-commands-stats config-toolbar-stats))
-  (let [commands (mobile-bar-commands parent-state parent-id)
-        sorted-commands (sort-by (comp :counts second) > @mobile-bar-commands-stats)]
-    [:div#mobile-editor-toolbar.bg-base-2
-     [:div.toolbar-commands
-      {:on-touch-start #(util/stop %)}
-      (mobile-bar-indent-outdent false "indent-decrease")
-      (mobile-bar-indent-outdent true "indent-increase")
-      (mobile-bar-command (editor-handler/move-up-down true) "arrow-bar-to-up")
-      (mobile-bar-command (editor-handler/move-up-down false) "arrow-bar-to-down")
-      (mobile-bar-command #(if (state/sub :document/mode?)
-                             (editor-handler/insert-new-block! nil)
-                             (commands/simple-insert! parent-id "\n" {})) "arrow-back")
-      (for [command sorted-commands]
-        ((first command) commands))]
-     [:div.toolbar-hide-keyboard
-      (mobile-bar-command #(state/clear-edit!) "keyboard-show")]]))
-
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
   (mixins/event-mixin
@@ -409,9 +321,9 @@
                :z-index    11}
               (when set-default-width?
                 {:width max-width})
-              (let [^js/HTMLElement textarea
-                    (js/document.querySelector "textarea.ls-textarea")]
-                (if (<= (.-clientWidth textarea) (+ left (if set-default-width? max-width 500)))
+              (let [^js/HTMLElement editor
+                    (js/document.querySelector ".editor-wrapper")]
+                (if (<= (.-clientWidth editor) (+ left (if set-default-width? max-width 500)))
                   {:right 0}
                   {:left (if (and y-diff (= y-diff 0)) left 0)})))}
      cp]))
@@ -478,7 +390,6 @@
   (let [content (if content (str content) "")]
     ;; as the function is binding to the editor content, optimization is welcome
     (str
-     "ls-textarea "
      (if (or (> (.-length content) 1000)
              (string/includes? content "\n"))
        "multiline-block"
@@ -609,7 +520,7 @@
   (mixins/event-mixin setup-key-listener!)
   (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
-  [state {:keys [format block]} id config]
+  [state {:keys [format block]} id _config]
   (let [content (state/sub-edit-content)
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
@@ -618,11 +529,6 @@
        [:div#audio-record-toolbar
         (footer/audio-record-cp)])
 
-     (when (and (or (mobile-util/is-native-platform?)
-                    config/mobile?)
-                (not (:review-cards? config)))
-       (mobile-bar state id))
-
      (ui/ls-textarea
       {:id                id
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
@@ -631,7 +537,6 @@
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
        :on-paste          (editor-handler/editor-on-paste! id)
-       :on-height-change  (editor-handler/editor-on-height-change! id)
        :auto-focus        false
        :class             heading-class})
 

+ 0 - 30
src/main/frontend/components/editor.css

@@ -1,33 +1,3 @@
-#mobile-editor-toolbar {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  width: 100%;
-  /* height: 2.5rem; */
-  z-index: 9999;
-  transition: none;
-  display: flex;
-  justify-content: space-between;
-
-  button {
-    padding: 7px 10px;
-  }
-  
-  .toolbar-commands {
-    justify-content: space-between;
-    display: flex;
-    align-items: center;
-    overflow-x: overlay;
-    overflow-y: hidden;
-    width: 95%;
-  }
-
-  .toolbar-hide-keyboard {
-    border-left: 1px solid;
-    border-color: var(--ls-quaternary-background-color);
-  }
-}
-
 #audio-record-toolbar {
     position: fixed;
     background-color: var(--ls-secondary-background-color);

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

@@ -9,11 +9,11 @@
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.format :as format]
             [frontend.handler.export :as export-handler]
             [frontend.state :as state]
             [frontend.util :as util]
             [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.util :as gp-util]
             [goog.object :as gobj]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
@@ -71,7 +71,7 @@
                    state)}
   [state]
   (let [path (get-path state)
-        format (format/get-format path)
+        format (gp-util/get-format path)
         original-name (db/get-file-page path)
         random-id (str (d/squuid))]
     [:div.file {:id (str "file-edit-wrapper-" random-id)}

+ 3 - 5
src/main/frontend/components/header.cljs

@@ -224,11 +224,10 @@
   (let [repos (->> (state/sub [:me :repos])
                    (remove #(= (:url %) config/local-repo)))
         electron-mac? (and util/mac? (util/electron?))
-        vw-state (state/sub :ui/visual-viewport-state)
         show-open-folder? (and (nfs/supported?)
                                (or (empty? repos)
                                    (nil? (state/sub :git/current-repo)))
-                               (not (mobile-util/is-native-platform?))
+                               (not (mobile-util/native-platform?))
                                (not config/publishing?))]
     [:div.cp__header#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
@@ -239,8 +238,7 @@
                            (when (and (util/electron?)
                                       (.. target -classList (contains "cp__header")))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
-      :style           {:fontSize  50
-                        :transform (str "translateY(" (or (:offset-top vw-state) 0) "px)")}}
+      :style           {:fontSize  50}}
      [:div.l.flex
       (left-menu-button {:on-click (fn []
                                      (open-fn)
@@ -271,7 +269,7 @@
                 (mobile-util/native-ios?))
         (back-and-forward))
 
-      (when-not (mobile-util/is-native-platform?)
+      (when-not (mobile-util/native-platform?)
         (new-block-mode))
 
       (when show-open-folder?

+ 4 - 1
src/main/frontend/components/header.css

@@ -16,6 +16,7 @@
   user-select: none;
   line-height: 1;
   white-space: nowrap;
+  background-color: var(--ls-primary-background-color);
 
   > .l {
     width: var(--ls-left-sidebar-width);
@@ -221,7 +222,9 @@ html.is-native-iphone-without-notch,
 html.is-native-ipad {
 
      #main-container {
-        padding-top: 0px;
+         padding-top: 0px;
+         display: flex;
+         flex-direction: column;
     }
 
      #main-content-container {

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

@@ -4,7 +4,7 @@
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.ui :as ui]
             [medley.core :as medley]
             [rum.core :as rum]

+ 6 - 4
src/main/frontend/components/journal.cljs

@@ -8,7 +8,8 @@
             [frontend.db.model :as model]
             [frontend.handler.page :as page-handler]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.object :as gobj]
@@ -51,7 +52,7 @@
                          :page))
                       (.preventDefault e)))}
        [:h1.title
-        (util/capitalize-all title)]]
+        (gp-util/capitalize-all title)]]
 
       (blocks-cp repo page format)
 
@@ -60,12 +61,13 @@
      (page/today-queries repo today? false)
 
      (rum/with-key
-       (reference/references title false)
+       (reference/references title)
        (str title "-refs"))]))
 
 (rum/defc journal-cp
   [journal]
-  (ui/lazy-visible nil (fn [] (journal-cp-inner journal)) nil))
+  (ui/lazy-visible (fn [] (journal-cp-inner journal)) nil
+                   {:reset-height? true}))
 
 (rum/defc journals < rum/reactive
   [latest-journals]

+ 2 - 2
src/main/frontend/components/onboarding/setups.cljs

@@ -71,10 +71,10 @@
       [:section.a
        [:strong "Let’s get you set up."]
        [:small (str "Where on your " DEVICE " do you want to save your work?")
-        (when (mobile-util/is-native-platform?)
+        (when (mobile-util/native-platform?)
           (mobile-intro))]
 
-       (if (or (nfs/supported?) (mobile-util/is-native-platform?))
+       (if (or (nfs/supported?) (mobile-util/native-platform?))
          [:div.choose.flex.flex-col.items-center
           {:on-click #(page-handler/ls-dir-files!
                        (fn []

+ 21 - 23
src/main/frontend/components/page.cljs

@@ -1,6 +1,6 @@
 (ns frontend.components.page
   (:require [clojure.string :as string]
-            [frontend.components.block :as block]
+            [frontend.components.block :as component-block]
             [frontend.components.content :as content]
             [frontend.components.editor :as editor]
             [frontend.components.hierarchy :as hierarchy]
@@ -15,7 +15,6 @@
             [frontend.db.model :as model]
             [frontend.extensions.graph :as graph]
             [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.format.block :as format-block]
             [frontend.handler.common :as common-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.editor :as editor-handler]
@@ -26,7 +25,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.search :as search]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -35,6 +34,7 @@
             [medley.core :as medley]
             [rum.core :as rum]
             [logseq.graph-parser.util :as gp-util]
+            [frontend.format.block :as block]
             [frontend.mobile.util :as mobile-util]))
 
 (defn- get-page-name
@@ -122,8 +122,8 @@
   (when page-e
     (let [page-name (or (:block/name page-e)
                         (str (:block/uuid page-e)))
-          block? (gp-util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           page-blocks (get-blocks repo page-name block-id)]
       (if (empty? page-blocks)
         (dummy-block page-name)
@@ -139,7 +139,7 @@
                               :document/mode? document-mode?}
                              config)
               hiccup-config (common-handler/config-with-document-mode hiccup-config)
-              hiccup (block/->hiccup page-blocks hiccup-config {})]
+              hiccup (component-block/->hiccup page-blocks hiccup-config {})]
           [:div
            (page-blocks-inner page-name page-blocks hiccup sidebar? block-id)
            (when-not config/publishing?
@@ -163,9 +163,9 @@
            (rum/with-key
              (ui/catch-error
               (ui/component-error "Failed default query:" {:content (pr-str query)})
-              (block/custom-query {:attr {:class "mt-10"}
-                                   :editor-box editor/box
-                                   :page page} query))
+              (component-block/custom-query {:attr {:class "mt-10"}
+                                             :editor-box editor/box
+                                             :page page} query))
              (str repo "-custom-query-" (:query query))))]))))
 
 (defn tagged-pages
@@ -177,7 +177,7 @@
         (ui/foldable
          [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
          [:ul.mt-2
-          (for [[original-name name] (sort pages)]
+          (for [[original-name name] (sort-by last pages)]
             [:li {:key (str "tagged-page-" name)}
              [:a {:href (rfe/href :page {:name name})}
               original-name]])]
@@ -222,10 +222,9 @@
                          (reset! *edit? false)
                          (notification/show! "Illegal page name, can not rename!" :warning))
           blur-fn (fn [e]
-                    (when (util/wrapped-by-quotes? @*title-value)
-                      (swap! *title-value util/unquote-string)
+                    (when (gp-util/wrapped-by-quotes? @*title-value)
+                      (swap! *title-value gp-util/unquote-string)
                       (gobj/set (rum/deref input-ref) "value" @*title-value))
-                    (state/set-state! :editor/editing-page-title? false)
                     (cond
                       (= old-name @*title-value)
                       (reset! *edit? false)
@@ -260,7 +259,6 @@
                               (reset! *title-value old-name)
                               (reset! *edit? false)))}]]
         [:a.page-title {:on-mouse-down (fn [e]
-                                         (state/set-state! :editor/editing-page-title? true)
                                          (when (util/right-click? e)
                                            (state/set-state! :page-title/context {:page page-name})))
                         :on-click (fn [e]
@@ -317,8 +315,8 @@
     (let [current-repo (state/sub :git/current-repo)
           repo (or repo current-repo)
           page-name (util/page-name-sanity-lc path-page-name)
-          block? (gp-util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
+          block? (boolean block-id)
           format (let [page (if block-id
                               (:block/name (:block/page (db/entity [:block/uuid block-id])))
                               page-name)]
@@ -332,7 +330,7 @@
                       (db/entity repo))
                  (do
                    (when-not (db/entity repo [:block/name page-name])
-                     (let [m (format-block/page-name->map path-page-name true)]
+                     (let [m (block/page-name->map path-page-name true)]
                        (db/transact! repo [m])))
                    (db/pull [:block/name page-name])))
           {:keys [icon]} (:block/properties page)
@@ -357,7 +355,7 @@
        [:div.relative
         (when (and (not sidebar?) (not block?))
           [:div.flex.flex-row.space-between
-           (when (or (mobile-util/is-native-platform?) (util/mobile?))
+           (when (or (mobile-util/native-platform?) (util/mobile?))
              [:div.flex.flex-row.pr-2
               {:style {:margin-left -15}
                :on-mouse-over (fn [e]
@@ -377,7 +375,7 @@
            (let [config {:id "block-parent"
                          :block? true}]
              [:div.mb-4
-              (block/breadcrumb config repo block-id {:level-limit 3})]))
+              (component-block/breadcrumb config repo block-id {:level-limit 3})]))
 
          ;; blocks
          (let [page (if block?
@@ -394,7 +392,7 @@
        ;; referenced blocks
        [:div {:key "page-references"}
         (rum/with-key
-          (reference/references route-page-name sidebar?)
+          (reference/references route-page-name)
           (str route-page-name "-refs"))]
 
        (when-not block?
@@ -640,7 +638,7 @@
               (date/today))
         theme (:ui/theme @state/state)
         dark? (= theme "dark")
-        graph (if (gp-util/uuid-string? page)
+        graph (if (util/uuid-string? page)
                 (graph-handler/build-block-graph (uuid page) theme)
                 (graph-handler/build-page-graph page theme))]
     (when (seq (:nodes graph))
@@ -712,7 +710,7 @@
          [:tr {:key name}
           [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
           [:td.name [:a {:href     (rfe/href :page {:name (:block/name page)})}
-                     (block/page-cp {} page)]]
+                     (component-block/page-cp {} page)]]
           [:td.backlinks [:span (or backlinks "0")]]
           (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
           (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]
@@ -954,7 +952,7 @@
                                                (:db/id page)
                                                :page))))
                               :href     (rfe/href :page {:name (:block/name page)})}
-                          (block/page-cp {} page)]]
+                          (component-block/page-cp {} page)]]
 
                (when-not mobile?
                  [:td.backlinks [:span backlinks]])

+ 6 - 6
src/main/frontend/components/page_menu.cljs

@@ -14,7 +14,6 @@
             [frontend.handler.shell :as shell]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.mobile.util :as mobile-util]
-            [logseq.graph-parser.util :as gp-util]
             [electron.ipc :as ipc]
             [frontend.config :as config]
             [frontend.handler.user :as user-handler]
@@ -64,7 +63,7 @@
           repo (state/sub :git/current-repo)
           page (db/entity repo [:block/name page-name])
           page-original-name (:block/original-name page)
-          block? (and page (gp-util/uuid-string? page-name))
+          block? (and page (util/uuid-string? page-name))
           contents? (= page-name "contents")
           properties (:block/properties page)
           public? (true? (:public properties))
@@ -85,7 +84,7 @@
                          (page-handler/unfavorite-page! page-original-name)
                          (page-handler/favorite-page! page-original-name)))}}
 
-          (when-not (mobile-util/is-native-platform?)
+          (when-not (mobile-util/native-platform?)
             {:title (t :page/presentation-mode)
              :options {:on-click (fn []
                                    (state/sidebar-add-block!
@@ -103,10 +102,11 @@
              {:title   (t :page/open-with-default-app)
               :options {:on-click #(js/window.apis.openPath file-path)}}])
 
-          (when (util/electron?)
+          (when (or (util/electron?)
+                    (mobile-util/native-platform?))
             {:title   (t :page/copy-page-url)
-              :options {:on-click #(util/copy-to-clipboard!
-                                    (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
+             :options {:on-click #(util/copy-to-clipboard!
+                                   (url-util/get-logseq-graph-page-url nil repo page-original-name))}})
 
           (when-not contents?
             {:title   (t :page/delete)

+ 72 - 47
src/main/frontend/components/plugins.cljs

@@ -17,62 +17,87 @@
             [clojure.string :as string]))
 
 (rum/defcs installed-themes
-  < rum/reactive
-    (rum/local 0 ::cursor)
-    (rum/local 0 ::total)
-    (mixins/event-mixin
-      (fn [state]
-        (let [*cursor (::cursor state)
-              *total (::total state)
-              ^js target (rum/dom-node state)]
-          (.focus target)
-          (mixins/on-key-down
-            state {38                                       ;; up
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (zero? @*cursor)
-                               (dec @*total) (dec @*cursor))))
-                   40                                       ;; down
-                   (fn [^js _e]
-                     (reset! *cursor
-                             (if (= @*cursor (dec @*total))
-                               0 (inc @*cursor))))
-
-                   13                                       ;; enter
-                   #(when-let [^js active (.querySelector target ".is-active")]
-                      (.click active))
-                   }))))
+  <
+  (rum/local [] ::themes)
+  (rum/local 0 ::cursor)
+  (rum/local 0 ::total)
+  {:did-mount (fn [state] (let [*themes        (::themes state)
+                                *cursor        (::cursor state)
+                                *total         (::total state)
+                                mode           (state/sub :ui/theme)
+                                all-themes     (state/sub :plugin/installed-themes)
+                                themes         (->> all-themes
+                                                    (filter #(= (:mode %) mode))
+                                                    (sort-by #(:name %)))
+                                no-mode-themes (->> all-themes
+                                                    (filter #(= (:mode %) nil))
+                                                    (sort-by #(:name %))
+                                                    (map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
+                                selected       (state/sub :plugin/selected-theme)
+                                themes         (map-indexed (fn [idx opt]
+                                                              (let [selected? (= (:url opt) selected)]
+                                                                (when selected? (reset! *cursor (+ idx 1)))
+                                                                (assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
+                                themes         (cons {:name        (string/join " " ["Default" (string/capitalize mode) "Theme"])
+                                                      :url         nil
+                                                      :description (string/join " " ["Logseq default" mode "theme."])
+                                                      :mode        mode
+                                                      :selected    (nil? selected)
+                                                      :group-first true
+                                                      :group-desc  (str mode " themes")} themes)]
+                            (reset! *themes themes)
+                            (reset! *total (count themes))
+                            state))}
+  (mixins/event-mixin
+   (fn [state]
+     (let [*cursor    (::cursor state)
+           *total     (::total state)
+           ^js target (rum/dom-node state)]
+       (.focus target)
+       (mixins/on-key-down
+        state {38                                       ;; up
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (zero? @*cursor)
+                           (dec @*total) (dec @*cursor))))
+               40                                       ;; down
+               (fn [^js _e]
+                 (reset! *cursor
+                         (if (= @*cursor (dec @*total))
+                           0 (inc @*cursor))))
+
+               13                                       ;; enter
+               #(when-let [^js active (.querySelector target ".is-active")]
+                  (.click active))}))))
   [state]
   (let [*cursor (::cursor state)
-        *total (::total state)
-        themes (state/sub :plugin/installed-themes)
-        selected (state/sub :plugin/selected-theme)
-        themes (cons {:name "Default Theme" :url nil :description "Logseq default light/dark theme."} themes)
-        themes (sort #(:selected %) (map #(assoc % :selected (= (:url %) selected)) themes))
-        _ (reset! *total (count themes))]
-
+        *themes (::themes state)]
     [:div.cp__themes-installed
      {:tab-index -1}
      [:h1.mb-4.text-2xl.p-1 (t :themes)]
      (map-indexed
-       (fn [idx opt]
-         (let [current-selected (:selected opt)
-               plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+      (fn [idx opt]
+        (let [current-selected? (:selected opt)
+              group-first?      (:group-first opt)
+              plg               (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+          [:div
+           (when (and group-first? (not= idx 0)) [:hr.my-2])
            [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
             {:key      (str idx (:url opt))
-             :title    (when current-selected "Cancel selected theme")
+             :title    (:description opt)
              :class    (util/classnames
-                         [{:is-selected current-selected
-                           :is-active   (= idx @*cursor)}])
-             :on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
+                        [{:is-selected current-selected?
+                          :is-active   (= idx @*cursor)}])
+             :on-click #(do (js/LSPluginCore.selectTheme (bean/->js opt))
                             (state/close-modal!))}
-            [:section
-             [:strong.block
-              [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
-              (:name opt)]]
-            [:small.flex-shrink-0.flex.items-center.opacity-10
-             (when current-selected (ui/icon "check"))]]))
-       themes)]))
+            [:div.flex.items-center.text-xs
+             [:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
+             [:div.name.ml-1 (:name opt)]]
+            (when (or group-first? current-selected?)
+              [:div.flex.items-center
+               (when group-first? [:small.opacity-60 (:group-desc opt)])
+               (when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
+      @*themes)]))
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]
@@ -708,7 +733,7 @@
         updates (state/all-available-coming-updates)]
 
     [:div.cp__plugins-waiting-updates
-     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (util/safe-parse-int (count updates)))]
+     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (count updates))]
 
      (if (seq updates)
        ;; lists

+ 3 - 8
src/main/frontend/components/plugins.css

@@ -590,7 +590,7 @@
     outline: none;
     padding: 1rem;
 
-    > .it {
+    .it {
       user-select: none;
       background-color: var(--ls-secondary-background-color);
       border: 1px solid transparent;
@@ -598,13 +598,8 @@
       cursor: pointer;
       opacity: .8;
 
-      > section {
-        line-height: 1.1em;
-
-        > strong {
-          font-size: 13px;
-          font-weight: 600;
-        }
+      .name {
+        font-weight: 600;
       }
 
       &.is-active {

+ 8 - 13
src/main/frontend/components/reference.cljs

@@ -12,8 +12,6 @@
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]
-            [medley.core :as medley]
             [rum.core :as rum]))
 
 (rum/defc filter-dialog-inner < rum/reactive
@@ -83,8 +81,7 @@
           default-collapsed? (>= (count refed-blocks-ids) threshold)
           filters-atom (get state ::filters)
           filter-state (rum/react filters-atom)
-          block? (gp-util/uuid-string? page-name)
-          block-id (and block? (uuid page-name))
+          block-id (parse-uuid page-name)
           page-name (string/lower-case page-name)
           journal? (date/valid-journal-title? (string/capitalize page-name))
           scheduled-or-deadlines (when (and journal?
@@ -94,8 +91,8 @@
       (when (or (seq refed-blocks-ids)
                 (seq scheduled-or-deadlines)
                 (seq filter-state))
-        [:div.references.mt-6.flex-1.flex-row
-         [:div.content
+        [:div.references.flex-1.flex-row
+         [:div.content.pt-6
           (when (seq scheduled-or-deadlines)
             (ui/foldable
              [:h2.font-bold.opacity-50 "SCHEDULED AND DEADLINE"]
@@ -143,8 +140,8 @@
                                   (db/get-block-referenced-blocks block-id)
                                   (db/get-page-referenced-blocks page-name))
                      filters (when (seq filter-state)
-                               (->> (group-by second filter-state)
-                                    (medley/map-vals #(map first %))))
+                               (-> (group-by second filter-state)
+                                   (update-vals #(map first %))))
                      filtered-ref-blocks (block-handler/filter-blocks repo ref-blocks filters true)
                      n-ref (apply +
                              (for [[_ rfs] filtered-ref-blocks]
@@ -166,16 +163,14 @@
               :title-trigger? true}))]]))))
 
 (rum/defc references
-  [page-name sidebar?]
+  [page-name]
   (ui/catch-error
    (ui/component-error "Linked References: Unexpected error")
    (ui/lazy-visible
-    (if (or sidebar? (gp-util/uuid-string? page-name))
-      nil
-      "loading references...")
     (fn []
       (references* page-name))
-    nil)))
+    nil
+    {:reset-height? false})))
 
 (rum/defcs unlinked-references-aux
   < rum/reactive db-mixins/query

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

@@ -14,7 +14,7 @@
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [promesa.core :as p]
             [electron.ipc :as ipc]
             [goog.object :as gobj]
@@ -44,7 +44,7 @@
        [:div.pl-1.content.mt-3
         [:div.flex.flex-row.my-4
          (when (or (nfs-handler/supported?)
-                   (mobile-util/is-native-platform?))
+                   (mobile-util/native-platform?))
            [:div.mr-8
             (ui/button
               (t :open-a-directory)
@@ -102,7 +102,7 @@
                        (when (and nfs-repo?
                                   (not= current-repo config/local-repo)
                                   (or (nfs-handler/supported?)
-                                      (mobile-util/is-native-platform?)))
+                                      (mobile-util/native-platform?)))
                          {:title (t :sync-from-local-files)
                           :hover-detail (t :sync-from-local-files-detail)
                           :options {:on-click

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

@@ -20,7 +20,6 @@
             [clojure.string :as string]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
-            [logseq.graph-parser.util :as gp-util]
             [reitit.frontend.easy :as rfe]
             [frontend.modules.shortcut.core :as shortcut]))
 
@@ -33,7 +32,7 @@
             lc-content (util/search-normalize content)
             lc-q (util/search-normalize q)]
         (if (and (string/includes? lc-content lc-q)
-                 (not (gp-util/safe-re-find #" " q)))
+                 (not (util/safe-re-find #" " q)))
           (let [i (string/index-of lc-content lc-q)
                 [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
             [:div

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

@@ -10,7 +10,7 @@
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.db :as db]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [rum.core :as rum]
             [frontend.config :as config]
             [frontend.handler.repo :as repo-handler]

+ 6 - 6
src/main/frontend/components/settings.cljs

@@ -132,7 +132,7 @@
                               :href     href
                               :on-click on-click))]
     (when-not (or (util/mobile?)
-                  (mobile-util/is-native-platform?))
+                  (mobile-util/native-platform?))
       [:div.text-sm desc])]])
 
 (defn edit-config-edn []
@@ -161,7 +161,7 @@
      (ui/toggle show-brackets?
                 config-handler/toggle-ui-show-brackets!
                 true)]]
-   (when (not (or (util/mobile?) (mobile-util/is-native-platform?)))
+   (when (not (or (util/mobile?) (mobile-util/native-platform?)))
      [:div {:style {:text-align "right"}}
       (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))])])
 
@@ -543,9 +543,9 @@
      (show-brackets-row t show-brackets?)
      (when (util/electron?) (switch-spell-check-row t))
      (outdenting-row t logical-outdenting?)
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (tooltip-row t enable-tooltip?))
      (timetracking-row t enable-timetracking?)
      (journal-row t enable-journals?)
@@ -595,7 +595,7 @@
     [:div.panel-wrap.is-advanced
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
-     (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
+     (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when (util/electron?) (plugin-system-switcher-row))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)
@@ -633,7 +633,7 @@
         (for [[label text icon]
               [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
                [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
-               (when-not (mobile-util/is-native-platform?)
+               (when-not (mobile-util/native-platform?)
                  [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
                [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
                (when plugins-of-settings

+ 48 - 18
src/main/frontend/components/sidebar.cljs

@@ -4,28 +4,34 @@
             [frontend.components.command-palette :as command-palette]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
+            [frontend.components.onboarding :as onboarding]
+            [frontend.components.plugins :as plugins]
             [frontend.components.repo :as repo]
             [frontend.components.right-sidebar :as right-sidebar]
+            [frontend.components.select :as select]
+            [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
-            [frontend.components.plugins :as plugins]
-            [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
-            [frontend.db.model :as db-model]
-            [frontend.components.svg :as svg]
             [frontend.db-mixins :as db-mixins]
+            [frontend.db.model :as db-model]
+            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.srs :as srs]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.route :as route-handler]
+            [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.mixins :as mixins]
+            [frontend.mobile.footer :as footer]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.mobile.mobile-bar :refer [mobile-bar]]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [reitit.frontend.easy :as rfe]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [rum.core :as rum]
@@ -36,6 +42,7 @@
             [frontend.components.onboarding :as onboarding]
             [frontend.mobile.footer :as footer]
             [frontend.mobile.action-bar :as action-bar]))
+            [reitit.frontend.easy :as rfe]))
 
 (rum/defc nav-content-item
   [name {:keys [class]} child]
@@ -301,8 +308,11 @@
        {:data-is-global-graph-pages global-graph-pages?
         :data-is-full-width         (or global-graph-pages?
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
-       
-       (when (and (not (mobile-util/is-native-platform?))
+
+       (mobile-bar)
+       (footer/footer)
+
+       (when (and (not (mobile-util/native-platform?))
                   (contains? #{:page :home} route-name))
          (widgets/demo-graph-alert))
        
@@ -345,6 +355,20 @@
                        (:current-parsing-file state))]]]]
     (ui/progress-bar-with-label width left-label (str finished "/" total))))
 
+(rum/defc file-sync-download-progress < rum/static
+  [state]
+  (let [finished (or (:finished state) 0)
+        total (:total state)
+        width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
+        left-label [:div.flex.flex-row.font-bold
+                    "Downloading"
+                    [:div.hidden.md:flex.flex-row
+                     [:span.mr-1 ": "]
+                     [:ul
+                      (for [file (:downloading-files state)]
+                        [:li file])]]]]
+    (ui/progress-bar-with-label width left-label (str finished "/" total))))
+
 (rum/defc main-content < rum/reactive db-mixins/query
   {:init (fn [state]
            (when-not @sidebar-inited?
@@ -361,15 +385,26 @@
                                               [page :page])]
                      (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
-           state)}
+           state)
+   :did-mount (fn [state]
+                (state/set-state! :mobile/show-tabbar? true)
+                state)}
   []
   (let [default-home (get-default-home-if-valid)
         current-repo (state/sub :git/current-repo)
         loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
         journals-length (state/sub :journals-length)
         latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
-        graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
+        graph-parsing-state (state/sub [:graph/parsing-state current-repo])
+        graph-file-sync-download-init-state (state/sub [:file-sync/download-init-progress current-repo])]
     (cond
+      (or
+       (:downloading? graph-file-sync-download-init-state)
+       (not= (:total graph-file-sync-download-init-state) (:finished graph-file-sync-download-init-state)))
+      [:div.flex.items-center.justify-center.full-height-without-header
+       [:div.flex-1
+        (file-sync-download-progress graph-file-sync-download-init-state)]]
+
       (or
        (:graph-loading? graph-parsing-state)
        (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
@@ -440,7 +475,8 @@
 (defn- hide-context-menu-and-clear-selection
   [e]
   (state/hide-custom-context-menu!)
-  (when-not (gobj/get e "shiftKey")
+  (when-not (or (gobj/get e "shiftKey")
+                (util/meta-key? e))
     (editor-handler/clear-selection!)))
 
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
@@ -526,13 +562,7 @@
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :db-restoring?       db-restoring?
-               :main-content        main-content
-               :show-action-bar?    show-action-bar?})
-
-        (when (and (mobile-util/is-native-platform?)
-                   current-repo
-                   (not (state/sub :modal/show?)))
-          (footer/footer))]
+               :main-content        main-content})]
 
        (right-sidebar/sidebar)
 

+ 1 - 0
src/main/frontend/components/theme.cljs

@@ -24,6 +24,7 @@
         (if (= theme "dark") ;; for tailwind dark mode
           (.add cls "dark")
           (.remove cls "dark"))
+        (ui/apply-custom-theme-effect! theme)
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
 

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

@@ -12,8 +12,8 @@
   []
   [:div.flex.flex-col
    [:h1.title (t :on-boarding/add-graph)]
-   (let [nfs-supported? (or (nfs/supported?) (mobile-util/is-native-platform?))]
-     (if (mobile-util/is-native-platform?)
+   (let [nfs-supported? (or (nfs/supported?) (mobile-util/native-platform?))]
+     (if (mobile-util/native-platform?)
        [:div.text-sm
         (ui/button "Open a local directory"
           :on-click #(page-handler/ls-dir-files! shortcut/refresh!))

+ 12 - 11
src/main/frontend/config.cljs

@@ -85,6 +85,15 @@
      config-formats
      #{:gif :svg :jpeg :ico :png :jpg :bmp :webp})))
 
+(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})))
+
 (def audio-formats #{:mp3 :ogg :mpeg :wav :m4a :flac :wma :aac})
 
 (def media-formats (set/union (img-formats) audio-formats))
@@ -97,17 +106,9 @@
   (set/union (text-formats)
              (img-formats)))
 
-;; TODO: rename
-(defonce mldoc-support-formats
-  #{:org :markdown :md})
-
-(defn mldoc-support?
-  [format]
-  (contains? mldoc-support-formats (keyword format)))
-
 (def mobile?
   (when-not util/node-test?
-    (gp-util/safe-re-find #"Mobi" js/navigator.userAgent)))
+    (util/safe-re-find #"Mobi" js/navigator.userAgent)))
 
 ;; TODO: protocol design for future formats support
 
@@ -314,7 +315,7 @@
     (and (util/electron?) (local-db? repo-url))
     (get-local-dir repo-url)
 
-    (and (mobile-util/is-native-platform?) (local-db? repo-url))
+    (and (mobile-util/native-platform?) (local-db? repo-url))
     (let [dir (get-local-dir repo-url)]
       (if (string/starts-with? dir "file:")
         dir
@@ -327,7 +328,7 @@
 
 (defn get-repo-path
   [repo-url path]
-  (if (and (or (util/electron?) (mobile-util/is-native-platform?))
+  (if (and (or (util/electron?) (mobile-util/native-platform?))
            (local-db? repo-url))
     path
     (util/node-path.join (get-repo-dir repo-url) path)))

+ 17 - 36
src/main/frontend/date.cljs

@@ -5,9 +5,10 @@
             [cljs-time.core :as t]
             [cljs-time.format :as tf]
             [cljs-time.local :as tl]
-            [clojure.string :as string]
             [frontend.state :as state]
             [frontend.util :as util]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.date-time-util :as date-time-util]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]))
 
@@ -16,11 +17,6 @@
   (when (string? s)
     ((gobj/get chrono "parseDate") s)))
 
-(defn format
-  [date]
-  (when-let [formatter-string (state/get-date-formatter)]
-    (tf/unparse (tf/formatter formatter-string) date)))
-
 (def custom-formatter (tf/formatter "yyyy-MM-dd'T'HH:mm:ssZZ"))
 
 (defn journal-title-formatters
@@ -55,13 +51,6 @@
      "yyyy年MM月dd日"}
    (state/get-date-formatter)))
 
-;; (tf/parse (tf/formatter "dd.MM.yyyy") "2021Q4") => 20040120T000000
-(defn safe-journal-title-formatters
-  []
-  (->> [(state/get-date-formatter) "yyyy-MM-dd" "yyyy_MM_dd"]
-       (remove string/blank?)
-       distinct))
-
 (defn get-date-time-string
   ([]
    (get-date-time-string (t/now)))
@@ -115,7 +104,7 @@
   ([]
    (journal-name (tl/local-now)))
   ([date]
-   (format date)))
+   (date-time-util/format date (state/get-date-formatter))))
 
 (defn journal-name-s [s]
   (try
@@ -183,34 +172,19 @@
 (defn valid-journal-title?
   [title]
   (and title
-       (valid? (util/capitalize-all title))))
+       (valid? (gp-util/capitalize-all title))))
 
 (defn journal-title->
   ([journal-title then-fn]
-   (journal-title-> journal-title then-fn (safe-journal-title-formatters)))
+   (journal-title-> journal-title then-fn (date-time-util/safe-journal-title-formatters (state/get-date-formatter))))
   ([journal-title then-fn formatters]
-   (when-not (string/blank? journal-title)
-     (when-let [time (->> (map
-                            (fn [formatter]
-                              (try
-                                (tf/parse (tf/formatter formatter) (util/capitalize-all journal-title))
-                                (catch js/Error _e
-                                  nil)))
-                            formatters)
-                          (filter some?)
-                          first)]
-       (then-fn time)))))
+   (date-time-util/journal-title-> journal-title then-fn formatters)))
 
 (defn journal-title->int
   [journal-title]
-  (when journal-title
-    (let [journal-title (util/capitalize-all journal-title)]
-      (journal-title-> journal-title #(util/parse-int (tf/unparse (tf/formatter "yyyyMMdd") %))))))
-
-(defn int->journal-title
-  [day]
-  (when day
-    (format (tf/parse (tf/formatter "yyyyMMdd") (str day)))))
+  (date-time-util/journal-title->int
+   journal-title
+   (date-time-util/safe-journal-title-formatters (state/get-date-formatter))))
 
 (defn journal-day->ts
   [day]
@@ -231,9 +205,16 @@
                     default-journal-title-formatter)]
     (journal-title-> journal-title #(tf/unparse formatter %))))
 
+(defn date->file-name
+  [date]
+  (let [formatter (if-let [format (state/get-journal-file-name-format)]
+                    (tf/formatter format)
+                    default-journal-title-formatter)]
+    (tf/unparse formatter date)))
+
 (defn journal-title->custom-format
   [journal-title]
-  (journal-title-> journal-title format))
+  (journal-title-> journal-title #(date-time-util/format % (state/get-date-formatter))))
 
 (defn int->local-time-2
   [n]

+ 3 - 3
src/main/frontend/db/conn.cljs

@@ -7,7 +7,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.config :as config]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [logseq.graph-parser.util :as gp-util]
             [datascript.core :as d]))
 
@@ -23,7 +23,7 @@
 (defn get-repo-name
   [repo]
   (cond
-    (mobile-util/is-native-platform?)
+    (mobile-util/native-platform?)
     (text/get-graph-name-from-path repo)
 
     (config/local-db? repo)
@@ -36,7 +36,7 @@
   "repo-path: output of `get-repo-name`"
   [repo-path]
   (if (or (util/electron?)
-          (mobile-util/is-native-platform?))
+          (mobile-util/native-platform?))
     (text/get-file-basename repo-path)
     repo-path))
 

+ 2 - 3
src/main/frontend/db/debug.cljs

@@ -1,6 +1,5 @@
 (ns frontend.db.debug
-  (:require [medley.core :as medley]
-            [frontend.db.utils :as db-utils]
+  (:require [frontend.db.utils :as db-utils]
             [frontend.db :as db]
             [datascript.core :as d]
             [frontend.util :as util]))
@@ -8,7 +7,7 @@
 ;; shortcut for query a block with string ref
 (defn qb
   [string-id]
-  (db-utils/pull [:block/uuid (medley/uuid string-id)]))
+  (db-utils/pull [:block/uuid (uuid string-id)]))
 
 (defn check-left-id-conflicts
   []

+ 85 - 9
src/main/frontend/db/model.cljs

@@ -12,7 +12,6 @@
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
-            [frontend.format :as format]
             [frontend.state :as state]
             [frontend.util :as util :refer [react]]
             [logseq.graph-parser.util :as gp-util]
@@ -294,7 +293,7 @@
       (:block/format page)
       (when-let [file (:block/file page)]
         (when-let [path (:file/path (db-utils/entity (:db/id file)))]
-          (format/get-format path)))))
+          (gp-util/get-format path)))))
    (state/get-preferred-format)
    :markdown))
 
@@ -410,6 +409,29 @@
                      f))
                  form))
 
+(defn get-sorted-page-block-ids
+  [page-id]
+  (let [root (db-utils/entity page-id)]
+    (loop [result []
+           children (sort-by-left (:block/_parent root) root)]
+      (if (seq children)
+        (let [child (first children)]
+          (recur (conj result (:db/id child))
+                 (concat
+                  (sort-by-left (:block/_parent child) child)
+                  (rest children))))
+        result))))
+
+(defn sort-page-random-blocks
+  "Blocks could be non consecutive."
+  [blocks]
+  (assert (every? #(= (:block/page %) (:block/page (first blocks))) blocks) "Blocks must to be in a same page.")
+  (let [page-id (:db/id (:block/page (first blocks)))
+        ;; TODO: there's no need to sort all the blocks
+        sorted-ids (get-sorted-page-block-ids page-id)
+        blocks-map (zipmap (map :db/id blocks) blocks)]
+    (keep blocks-map sorted-ids)))
+
 (defn has-children?
   ([block-id]
    (has-children? (conn/get-db) block-id))
@@ -585,13 +607,68 @@
               (recur parent)))
           false)))))
 
+(defn get-prev-sibling
+  [db id]
+  (when-let [e (d/entity db id)]
+    (let [left (:block/left e)]
+      (when (not= (:db/id left) (:db/id (:block/parent e)))
+        left))))
+
+(defn get-right-sibling
+  [db db-id]
+  (when-let [block (d/entity db db-id)]
+    (get-by-parent-&-left db
+                          (:db/id (:block/parent block))
+                          db-id)))
+
+(defn last-child-block?
+  "The child block could be collapsed."
+  [db parent-id child-id]
+  (when-let [child (d/entity db child-id)]
+    (cond
+      (= parent-id child-id)
+      true
+
+      (get-right-sibling db child-id)
+      false
+
+      :else
+      (last-child-block? db parent-id (:db/id (:block/parent child))))))
+
+(defn- consecutive-block?
+  [block-1 block-2]
+  (let [db (conn/get-db)
+        aux-fn (fn [block-1 block-2]
+                 (and (= (:block/page block-1) (:block/page block-2))
+                      (or
+                       ;; sibling or child
+                       (= (:db/id (:block/left block-2)) (:db/id block-1))
+                       (when-let [prev-sibling (get-prev-sibling db (:db/id block-2))]
+                         (last-child-block? db (:db/id prev-sibling) (:db/id block-1))))))]
+    (or (aux-fn block-1 block-2) (aux-fn block-2 block-1))))
+
+(defn get-non-consecutive-blocks
+  [blocks]
+  (vec
+   (keep-indexed
+    (fn [i _block]
+      (when (< (inc i) (count blocks))
+        (when-not (consecutive-block? (nth blocks i)
+                                      (nth blocks (inc i)))
+          (nth blocks i))))
+    blocks)))
+
 (defn- get-start-id-for-pagination-query
   [repo-url current-db {:keys [db-before tx-meta] :as tx-report}
    result outliner-op page-id block-id tx-block-ids]
   (let [db-before (or db-before current-db)
         cached-ids (map :db/id @result)
         cached-ids-set (set (conj cached-ids page-id))
-        first-changed-id (if (= outliner-op :move-blocks)
+        first-changed-id (cond
+                           (= (:real-outliner-op tx-meta) :indent-outdent)
+                           (last (:move-blocks tx-meta))
+
+                           (= outliner-op :move-blocks)
                            (let [{:keys [move-blocks target from-page to-page]} tx-meta]
                              (cond
                                (= page-id target) ; move to the first block
@@ -612,6 +689,7 @@
                                        (when (seq others)
                                          (recur others)))
                                      nil)))))
+                           :else
                            (let [insert? (= :insert-blocks outliner-op)]
                              (some #(when (and (or (and insert? (not (contains? cached-ids-set %)))
                                                    true)
@@ -636,7 +714,6 @@
         (let [start-page? (:block/name (db-utils/entity start-id))]
           (when-not start-page?
             (let [previous-blocks (take-while (fn [b] (not= start-id (:db/id b))) @result)
-                  previous-count (count previous-blocks)
                   limit 25
                   more (get-paginated-blocks-no-cache current-db start-id {:limit limit
                                                                            :include-start? true
@@ -913,8 +990,8 @@
 
 (defn get-page
   [page-name]
-  (if (gp-util/uuid-string? page-name)
-    (db-utils/entity [:block/uuid (uuid page-name)])
+  (if-let [id (parse-uuid page-name)]
+    (db-utils/entity [:block/uuid id])
     (db-utils/entity [:block/name (util/page-name-sanity-lc page-name)])))
 
 (defn get-redirect-page-name
@@ -1222,9 +1299,8 @@
 
 (defn get-referenced-blocks-ids
   [page-name-or-block-uuid]
-  (if (gp-util/uuid-string? (str page-name-or-block-uuid))
-    (let [id (uuid page-name-or-block-uuid)]
-      (get-block-referenced-blocks-ids id))
+  (if-let [id (parse-uuid (str page-name-or-block-uuid))]
+    (get-block-referenced-blocks-ids id)
     (get-page-referenced-blocks-ids page-name-or-block-uuid)))
 
 (defn get-matched-blocks

+ 10 - 10
src/main/frontend/db/query_dsl.cljs

@@ -6,15 +6,15 @@
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]
+            [frontend.state :as state]
             [frontend.date :as date]
             [frontend.db.model :as model]
             [frontend.db.query-react :as query-react]
             [frontend.db.utils :as db-utils]
             [frontend.db.rules :as rules]
             [frontend.template :as template]
-            [frontend.text :as text]
-            [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]))
+            [logseq.graph-parser.text :as text]
+            [frontend.util :as util]))
 
 
 ;; Query fields:
@@ -67,7 +67,7 @@
           (date/journal-title->int input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years
@@ -99,7 +99,7 @@
           (date/journal-title->long input)))
 
       :else
-      (let [duration (util/parse-int (subs input 0 (dec (count input))))
+      (let [duration (parse-long (subs input 0 (dec (count input))))
             kind (last input)
             tf (case kind
                  "y" t/years
@@ -238,7 +238,7 @@
   (let [k (string/replace (name (nth e 1)) "_" "-")
         v (nth e 2)
         v (if-not (nil? v)
-            (text/parse-property k v)
+            (text/parse-property k v (state/get-config))
             v)
         v (if (coll? v) (first v) v)]
     {:query (list 'property '?b (keyword k) v)
@@ -283,7 +283,7 @@
   (let [[k v] (rest e)
         k (string/replace (name k) "_" "-")]
     (if (some? v)
-      (let [v' (text/parse-property k v)
+      (let [v' (text/parse-property k v (state/get-config))
             val (if (coll? v') (first v') v')]
         {:query (list 'page-property '?p (keyword k) val)
          :rules [:page-property]})
@@ -310,8 +310,8 @@
   [e sample]
   (when-let [num (second e)]
     (when (integer? num)
-      (reset! sample num))
-    nil))
+      (reset! sample num)
+      {:query [['?p :block/uuid]]})))
 
 (defn- build-sort-by
   [e sort-by_]
@@ -448,7 +448,7 @@ Some bindings in this fn:
                                                  (remove string/blank?)
                                                  (map (fn [x]
                                                         (if (or (contains? #{"+" "-"} (first x))
-                                                                (and (gp-util/safe-re-find #"\d" (first x))
+                                                                (and (util/safe-re-find #"\d" (first x))
                                                                      (some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
                                                           (keyword (name x))
                                                           x)))

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

@@ -10,9 +10,8 @@
             [frontend.debug :as debug]
             [frontend.extensions.sci :as sci]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]
             [lambdaisland.glogi :as log]))
 
 (defn resolve-input
@@ -32,14 +31,14 @@
     ;; This sometimes runs when there isn't a current page e.g. :home route
     (some-> (state/get-current-page) string/lower-case)
     (and (keyword? input)
-         (gp-util/safe-re-find #"^\d+d(-before)?$" (name input)))
+         (util/safe-re-find #"^\d+d(-before)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (subs input 0 (dec (count input))))]
       (date->int (t/minus (t/today) (t/days days))))
     (and (keyword? input)
-         (gp-util/safe-re-find #"^\d+d(-after)?$" (name input)))
+         (util/safe-re-find #"^\d+d(-after)?$" (name input)))
     (let [input (name input)
-          days (util/parse-int (subs input 0 (dec (count input))))]
+          days (parse-long (subs input 0 (dec (count input))))]
       (date->int (t/plus (t/today) (t/days days))))
 
     (and (string? input) (text/page-ref? input))

+ 1 - 2
src/main/frontend/db/react.cljs

@@ -10,7 +10,6 @@
             [frontend.db.utils :as db-utils]
             [frontend.state :as state]
             [frontend.util :as util :refer [react]]
-            [logseq.graph-parser.util :as gp-util]
             [cljs.spec.alpha :as s]
             [clojure.core.async :as async]))
 
@@ -230,7 +229,7 @@
         affected-keys (concat
                        (mapcat
                         (fn [block-id]
-                          (let [block-id (if (and (string? block-id) (gp-util/uuid-string? block-id))
+                          (let [block-id (if (and (string? block-id) (util/uuid-string? block-id))
                                            [:block/uuid block-id]
                                            block-id)]
                             (when-let [block (db-utils/entity block-id)]

+ 1 - 2
src/main/frontend/db/utils.cljs

@@ -4,7 +4,6 @@
             [frontend.state :as state]
             [clojure.string :as string]
             [datascript.transit :as dt]
-            [frontend.util :as util]
             [frontend.date :as date]
             [frontend.db.conn :as conn]
             [frontend.config :as config]
@@ -46,7 +45,7 @@
 
 (defn date->int
   [date]
-  (util/parse-int
+  (parse-long
    (string/replace (date/ymd date) "/" "")))
 
 (defn entity

+ 92 - 91
src/main/frontend/dicts.cljc

@@ -3073,28 +3073,28 @@
                           :default "tutorial-en.md")
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-en.md")
                                  :default "dummy-notes-en.md")
-        :on-boarding/demo-graph "Questo è un diagramma dimostrativo, le modifiche non saranno salvate finchè non aprirai una cartella locale."
-        :on-boarding/add-graph "Aggiungi un diagramma"
+        :on-boarding/demo-graph "Questo è un grafo dimostrativo, le modifiche non saranno salvate finché non aprirai una cartella locale."
+        :on-boarding/add-graph "Aggiungi un grafo"
         :on-boarding/open-local-dir "Apri una cartella locale"
-        :on-boarding/new-graph-desc-1 "Logseq supporta sia Markdown che Org-mode. Puoi aprire una cartella esistente o crearne una nuova sul tuo dispositivo. I tuoi dati saranno salvati in questo dispositivo."
+        :on-boarding/new-graph-desc-1 "Logseq supporta sia Markdown che Org-mode. Puoi aprire una cartella esistente o crearne una nuova sul tuo dispositivo. I tuoi dati saranno salvati solo sul tuo dispositivo."
         :on-boarding/new-graph-desc-2 "Dopo che hai aperto la tua cartella, saranno create al suo interno tre cartelle:"
-        :on-boarding/new-graph-desc-3 "/journals - conserva le tue pagine quotidiane"
-        :on-boarding/new-graph-desc-4 "/pages - conserva le altre pagine"
-        :on-boarding/new-graph-desc-5 "/logseq - conserva la configurazione, custom.css, e alcuni metadati."
-        :help/start "Per cominciare"
-        :help/about "Riguardo a Logseq"
+        :on-boarding/new-graph-desc-3 "/journals - contiene le pagine del diario giornaliero"
+        :on-boarding/new-graph-desc-4 "/pages - contiene le altre pagine"
+        :on-boarding/new-graph-desc-5 "/logseq - contiene i dati di configurazione, custom.css, e alcuni metadati."
+        :help/start "Per iniziare"
+        :help/about "Informazioni su Logseq"
         :help/roadmap "Roadmap"
-        :help/bug "Riportare un errore"
-        :help/feature "Richiedere una funzione"
+        :help/bug "Segnala un problema"
+        :help/feature "Richiedi una funzionalità"
         :help/changelog "Registro delle modifiche"
         :help/blog "blog di Logseq"
         :help/docs "Documentazione"
         :help/privacy "Politica sulla riservatezza"
-        :help/terms "Termini"
+        :help/terms "Termini di Servizio"
         :help/community "Comunità su Discord"
-        :help/awesome-logseq "Fantastico Logseq"
+        :help/awesome-logseq "Awesome Logseq"
         :help/shortcuts "Scorciatoie da tastiera"
-        :help/shortcuts-triggers "Inneschi"
+        :help/shortcuts-triggers "Attivazione delle scorciatoie"
         :help/shortcut "Scorciatoia"
         :help/slash-autocomplete "Barra di completamento automatico"
         :help/block-content-autocomplete "Autocompletamento del contenuto di blocco"
@@ -3106,12 +3106,12 @@
         :undo "Annulla"
         :redo "Ripeti"
         :general "Generale"
-        :more "Ancora"
+        :more "Altro"
         :search/result-for "Cerca i risultati per "
         :search/items "oggetti"
-        :search/page-names "Cerca nomi di pagina"
+        :search/page-names "Cerca pagine per nome"
         :help/context-menu "Menu contestuale del blocco"
-        :help/fold-unfold "Comprimi/Decomprimi blocchi (quando non sei in modalità di modifica)"
+        :help/fold-unfold "Comprimi/Espandi blocchi (quando non sei in modalità di modifica)"
         :help/markdown-syntax "Sintassi Markdown"
         :help/org-mode-syntax "Sintassi Org mode"
         :bold "Grassetto"
@@ -3131,17 +3131,17 @@
         :right-side-bar/block-ref "Riferimento di Blocco"
         :right-side-bar/graph-view "Vista del grafico"
         :right-side-bar/all-pages "Tutte le pagine"
-        :right-side-bar/flashcards "Carte flash"
+        :right-side-bar/flashcards "Flashcard"
         :right-side-bar/new-page "Nuova pagina"
-        :left-side-bar/journals "Diari"
+        :left-side-bar/journals "Diario"
         :left-side-bar/new-page "Nuova pagina"
         :left-side-bar/nav-favorites "Preferiti"
-        :left-side-bar/nav-shortcuts "Scrciatoie"
+        :left-side-bar/nav-shortcuts "Scorciatoie"
         :left-side-bar/nav-recent-pages "Recenti"
-        :format/preferred-mode "Qual'è la tua modalità preferita?"
+        :format/preferred-mode "Qual è la tua modalità preferita?"
         :format/markdown "Markdown"
         :format/org-mode "Org mode"
-        :reference/linked "Riferimento collegato"
+        :reference/linked "Riferimenti collegati"
         :reference/unlinked-ref "Riferimenti non collegati"
         :page/presentation-mode "Presentazione"
         :page/edit-properties-placeholder "Proprietà"
@@ -3152,41 +3152,41 @@
         :page/copy-to-json "Copia l'intera pagina in JSON"
         :page/rename "Rinomina pagina"
         :page/open-in-finder "Apri nella cartella"
-        :page/open-with-default-app "Apri con la app predefinita"
+        :page/open-with-default-app "Apri con l'app predefinita"
         :page/action-publish "Pubblica"
-        :page/make-public "Rendi pubblico per la pubblicazione"
+        :page/make-public "Segna come pubblico per la pubblicazione"
         :page/version-history "Controlla la cronologia della pagina"
         :page/open-backup-directory "Apri la cartella dei backup delle pagine"
         :page/file-sync-versions "Versioni delle pagine"
-        :page/make-private "Rendi privato"
+        :page/make-private "Segna come privato"
         :page/delete "Elimina pagina"
         :page/add-to-favorites "Aggiungi ai Preferiti"
         :page/unfavorite "Rimuovi la pagina dai Preferiti"
-        :page/show-journals "Mostra Diari"
+        :page/show-journals "Mostra diario"
         :page/show-name "Mostra il nome della pagina"
         :page/hide-name "Nascondi il nome della pagina"
         :block/name "Nome pagina"
         :page/last-modified "Ultima modifica alle"
-        :page/new-title "Qual'è il titolo della tua nuova pagina?"
+        :page/new-title "Qual è il titolo della tua nuova pagina?"
         :page/earlier "Prima"
         :page/no-more-journals "Non ci sono altri diari"
         :page/copy-page-url "Copia URL pagina"
-        :journal/multiple-files-with-different-formats "Sembra che tu abbia più file Diario (con formati diversi) per lo stesso mese, per favore conserva un solo file journal per ogni mese."
+        :journal/multiple-files-with-different-formats "Sembra che tu abbia più file diario (con formati diversi) per lo stesso mese, per favore conserva un solo file diario per ogni mese."
         :journal/go-to "Vai ai file"
         :file/name "Nome File"
         :file/file "File: "
         :file/last-modified-at "Ultima modifica alle"
-        :file/no-data "No dati"
+        :file/no-data "Nessun dato"
         :file/format-not-supported "Il formato .{1} non è supportato."
         :page/created-at "Creato alle"
         :page/updated-at "Aggiornato alle"
         :page/backlinks "Collegamenti a ritroso"
         :editor/block-search "Cerca un blocco"
-        :editor/image-uploading "Caricando"
+        :editor/image-uploading "Caricamento"
         :draw/invalid-file "Non ho potuto caricare questo file excalidraw"
         :draw/specify-title "Per favore specifica un titolo prima!"
         :draw/rename-success "Il file è stato rinominato con successo!"
-        :draw/rename-failure "La rinomina del file è fallita, ragione: "
+        :draw/rename-failure "La rinominazione del file è fallita, ragione: "
         :draw/title-placeholder "Senza titolo"
         :draw/save "Salva"
         :draw/save-changes "Salva modifiche"
@@ -3197,7 +3197,7 @@
         :draw/back-to-logseq "Torna a logseq"
         :text/image "Immagine"
         :asset/confirm-delete "Sei sicuro di voler eliminare questo {1}?"
-        :asset/physical-delete "Rimuovi anche i file (nota che non può essere ripristinato)"
+        :asset/physical-delete "Rimuovi anche i file (non possono essere ripristinati)"
         :content/copy "Copia"
         :content/cut "Taglia"
         :content/make-todos "Crea {1}"
@@ -3219,19 +3219,19 @@
         :settings-page/spell-checker "Correttore ortografico"
         :settings-page/auto-updater "Aggiornamento automatico"
         :settings-page/disable-sentry "Invia dati di utilizzo e diagnostica a Logseq"
-        :settings-page/preferred-outdenting "Distacco logico"
+        :settings-page/preferred-outdenting "Indentamento logico"
         :settings-page/custom-date-format "Formato data preferito"
         :settings-page/preferred-file-format "Formato file preferito"
         :settings-page/preferred-workflow "Flusso di lavoro preferito"
         :settings-page/enable-shortcut-tooltip "Abilita suggerimenti scorciatoie"
         :settings-page/enable-timetracking "Tracciamento del tempo"
         :settings-page/enable-tooltip "Suggerimenti"
-        :settings-page/enable-journals "Diari"
+        :settings-page/enable-journals "Diario"
         :settings-page/enable-all-pages-public "Tutte le pagine pubbliche durante la pubblicazione"
-        :settings-page/customize-shortcuts "Scorciatoie di tastiera"
+        :settings-page/customize-shortcuts "Scorciatoie da tastiera"
         :settings-page/shortcut-settings "Personalizza scorciatoie"
         :settings-page/home-default-page "Imposta la home page predefinita"
-        :settings-page/enable-block-time "Marche temporali sui blocchi"
+        :settings-page/enable-block-time "Indicatori temporali sui blocchi"
         :settings-page/clear-cache "Pulisci cache"
         :settings-page/clear "Pulisci"
         :settings-page/developer-mode "Modalità sviluppatore"
@@ -3239,19 +3239,19 @@
         :settings-page/disable-developer-mode "Disabilita modalità sviluppatore"
         :settings-page/developer-mode-desc "La modalità sviluppatore aiuta i contributori e gli sviluppatori di estensioni a testare le loro integrazioni con Logseq in modo più efficiente."
         :settings-page/current-version "Versione attuale"
-        :settings-page/current-graph "Diagramma attuale"
+        :settings-page/current-graph "Grafo attuale"
         :settings-page/tab-general "Generale"
         :settings-page/tab-editor "Editor"
         :settings-page/tab-shortcuts "Scorciatoie"
-        :settings-page/tab-version-control "Controllo versione"
+        :settings-page/tab-version-control "Controllo di versione"
         :settings-page/tab-advanced "Avanzate"
-        :settings-page/plugin-system "Sistema dei Plug-in"
+        :settings-page/plugin-system "Sistema di plugin"
         :settings-page/network-proxy "Proxy di rete"
         :logseq "Logseq"
         :on "ON"
         :more-options "Più opzioni"
         :to "a"
-        :yes "Si"
+        :yes "Sì"
         :no "No"
         :submit "Invia"
         :cancel "Annulla"
@@ -3262,63 +3262,63 @@
         :host "Host"
         :port "Porta"
         :re-index "Re-indicizza"
-        :re-index-detail "Ricostruisci il diagramma"
-        :re-index-multiple-windows-warning "È necessario chiudere le altre finestre prima di reindicizzare questo diagramma."
-        :re-index-discard-unsaved-changes-warning "Reindicizza elimina il diagramma corrente, quindi elabora nuovamente tutti i file, poiché sono attualmente archiviati su disco. Perderai le modifiche non salvate e potrebbe volerci del tempo. Continuare?"
+        :re-index-detail "Ricostruisci il grafo"
+        :re-index-multiple-windows-warning "È necessario chiudere le altre finestre prima di reindicizzare questo grafo."
+        :re-index-discard-unsaved-changes-warning "La reindicizzazione elimina il grafo corrente, quindi elabora nuovamente tutti i file poiché sono attualmente archiviati su disco. Perderai le modifiche non salvate e potrebbe volerci del tempo. Continuare?"
         :open-new-window "Nuova finestra"
         :sync-from-local-files "Ricarica"
         :sync-from-local-files-detail "Importa cambiamenti da un file locale"
-        :sync-from-local-changes-detected "Ricarica rileva ed elabora i file modificati sul disco e divergenti dal contenuto effettivo della pagina Logseq. Continuare?"
+        :sync-from-local-changes-detected "Il ricaricamento rileva ed elabora i file modificati sul disco e divergenti dal contenuto effettivo della pagina Logseq. Continuare?"
 
         :unlink "disconnetti"
         :search/publishing "Cerca"
         :search "Cerca o crea una pagina"
         :page-search "Cerca nella pagina corrente"
-        :graph-search "Cerca diagramma"
+        :graph-search "Cerca nel grafo"
         :new-page "Nuova pagina"
         :new-file "Nuovo file"
-        :new-graph "Aggiungi nuovo diagramma"
-        :graph "Diagramma"
-        :graph-view "Visualizza diagramma"
-        :graph/persist "Logseq sta sincronizzando lo stato interno, per favore aspetta molti secondi."
+        :new-graph "Aggiungi nuovo grafo"
+        :graph "Grafo"
+        :graph-view "Visualizza grafo"
+        :graph/persist "Logseq sta sincronizzando lo stato interno, per favore attendi alcuni secondi."
         :graph/persist-error "Sincronizzazione dello stato interno fallita."
-        :graph/save "Salvando..."
+        :graph/save "Salvataggio..."
         :graph/save-success "Salvato con successo"
         :graph/save-error "Salvataggio fallito"
-        :cards-view "Visualizza carte"
+        :cards-view "Visualizza flashcard"
         :publishing "Pubblicazione"
         :export "Esporta"
-        :export-graph "Esporta diagramma"
+        :export-graph "Esporta grafo"
         :export-page "Esporta pagina"
-        :export-markdown "Esporta come Markdown standard (nessuna proprietà di blocco)"
+        :export-markdown "Esporta come Markdown standard (senza le proprietà dei blocchi)"
         :export-opml "Esporta come OPML"
-        :export-public-pages "Esporta pagine pubbliche"
+        :export-public-pages "Esporta le pagine pubbliche"
         :export-json "Esporta come JSON"
         :export-roam-json "Esporta come Roam JSON"
         :export-edn "Esporta come EDN"
         :export-datascript-edn "Esporta datascript EDN"
         :convert-markdown "Converti le intestazioni di Markdown in elenchi non ordinati (# -> -)"
-        :all-graphs "Tutti i diagrammi"
+        :all-graphs "Tutti i grafi"
         :all-pages "Tutte le pagine"
         :all-files "Tutti i file"
         :remove-orphaned-pages "Rimuovi pagine orfane"
-        :all-journals "Tutti i diari"
+        :all-journals "Tutte le pagine di diario"
         :my-publishing "Le mie pubblicazioni"
         :settings "Impostazioni"
         :settings-of-plugins "Impostazioni plugin"
         :plugins "Plugin"
         :themes "Temi"
-        :developer-mode-alert "È necessario riavviare l'app per abilitare il plug-in. Vuoi riavviarlo ora?"
-        :relaunch-confirm-to-work "Bisogna riavviare l'app per farla funzionare. Vuoi riavviarla ora?"
+        :developer-mode-alert "È necessario riavviare l'app per abilitare il plugin. Vuoi riavviarla ora?"
+        :relaunch-confirm-to-work "È necessario riavviare l'app per farla funzionare. Vuoi riavviarla ora?"
         :import "Importa"
         :join-community "Unisciti alla comunità"
-        :sponsor-us "Sponsorizzaci"
+        :sponsor-us "Supportaci"
         :discord-title "Il nostro gruppo Discord!"
-        :help-shortcut-title "Clicca per controllare le scorciatoie e altri suggerimenti"
-        :loading "Caricando"
-        :cloning "Clonando"
-        :parsing-files "Analizzando i file"
-        :loading-files "Caricando i file"
+        :help-shortcut-title "Clicca per conoscere le scorciatoie e altri suggerimenti"
+        :loading "Caricamento"
+        :cloning "Clonazione"
+        :parsing-files "Analisi dei file"
+        :loading-files "Caricamento dei file"
         :login "Accedi"
         :logout "Esci"
         :go-to "Vai a "
@@ -3331,41 +3331,41 @@
         :remove-background "Rimuovi lo sfondo"
         :open "Apri"
         :open-a-directory "Apri una cartella locale"
-        :user/delete-account "Elimina account"
-        :user/delete-your-account "Elimina il tuo account"
+        :user/delete-account "Elimina profilo"
+        :user/delete-your-account "Elimina il tuo profilo"
         :user/delete-account-notice "Tutte le tue pagine pubblicate su logseq.com saranno eliminate."
 
-        :help/shortcut-page-title "Scorciatoie di tastiera"
+        :help/shortcut-page-title "Scorciatoie da tastiera"
 
         :plugin/installed "Installato"
         :plugin/not-installed "Non installato"
-        :plugin/installing "Installando"
+        :plugin/installing "Installazione"
         :plugin/install "Installa"
         :plugin/reload "Ricarica"
         :plugin/update "Aggiorna"
         :plugin/check-update "Controlla aggiornamenti"
         :plugin/check-all-updates "Controlla tutti gli aggiornamenti"
-        :plugin/refresh-lists "Ricarica liste"
+        :plugin/refresh-lists "Ricarica lista"
         :plugin/enabled "Abilitato"
         :plugin/disabled "Disabilitato"
         :plugin/update-available "Aggiornamento disponibile"
-        :plugin/updating "Aggiornando"
-        :plugin/uninstall "Disinstallare"
-        :plugin/marketplace "Mercato"
-        :plugin/downloads "Downloads"
+        :plugin/updating "Aggiornamento"
+        :plugin/uninstall "Disinstalla"
+        :plugin/marketplace "Libreria"
+        :plugin/downloads "Numero di scaricamenti"
         :plugin/stars "Stelle"
         :plugin/title "Titolo"
         :plugin/all "Tutti"
-        :plugin/unpacked "Spacchettati"
-        :plugin/delete-alert "Sei sicuro di disinstallare [{1}]?"
+        :plugin/unpacked "Non pacchettizzati"
+        :plugin/delete-alert "Sei sicuro di voler disinstallare [{1}]?"
         :plugin/open-settings "Apri impostazioni"
         :plugin/open-package "Apri pacchetto"
-        :plugin/load-unpacked "Carica plugin non impacchettato"
-        :plugin/open-preferences "Apri il file preferenze del plugin"
-        :plugin/restart "Riavvia App"
+        :plugin/load-unpacked "Carica plugin non pacchettizzato"
+        :plugin/open-preferences "Apri il file delle preferenze del plugin"
+        :plugin/restart "Riavvia app"
         :plugin/unpacked-tips "Seleziona la cartella del plugin"
-        :plugin/contribute "✨ Scrivi e sottoponi un nuovo plugin"
-        :plugin/marketplace-tips "Se il plug-in non funziona correttamente alla prima installazione, provare a riavviare Logseq."
+        :plugin/contribute "✨ Svilupppa e sottoponici un nuovo plugin"
+        :plugin/marketplace-tips "Se il plugin non funziona correttamente alla prima installazione, provare a riavviare Logseq."
         :plugin/up-to-date "È aggiornato"
         :plugin/custom-js-alert "Trovato il file custom.js, è consentito eseguirlo? (Se non si comprende il contenuto di questo file, si consiglia di non consentire l'esecuzione, che presenta alcuni rischi per la sicurezza.)"
 
@@ -3374,23 +3374,23 @@
         :pdf/linked-ref "Riferimenti collegati"
         :pdf/toggle-dashed "Stile tratteggiato per evidenziare l'area"
 
-        :updater/new-version-install "Una nuova versione è stata caricata."
-        :updater/quit-and-install "Riavvia per installare"
+        :updater/new-version-install "Una nuova versione è stata scaricata."
+        :updater/quit-and-install "Riavvia per installarla"
 
         :paginates/pages "Totale {1} pagine"
         :paginates/prev "Precedente"
-        :paginates/next "Prossimo"
+        :paginates/next "Successivo"
 
-        :tips/all-done "Tutto Fatto"
+        :tips/all-done "Completato"
 
-        :command-palette/prompt "Scrivi un comando"
-        :select/default-prompt "Seleziona uno"
-        :select.graph/prompt "Seleziona un diagramma"
-        :select.graph/empty-placeholder-description "Non ci sono diagrammi corrispondenti. Vuoi aggiungerne uno nuovo?"
-        :select.graph/add-graph "Si, aggiungi un nuovo diagramma"
+        :command-palette/prompt "Digita un comando"
+        :select/default-prompt "Selezionane uno"
+        :select.graph/prompt "Seleziona un grafo"
+        :select.graph/empty-placeholder-description "Non ci sono grafi corrispondenti. Vuoi aggiungerne uno nuovo?"
+        :select.graph/add-graph "Sì, aggiungi un nuovo grafo"
 
-        :file-sync/other-user-graph "Il diagramma locale attuale è associato al diagramma remoto di un altro utente. Quindi non è possibile avviare la sincronizzazione."
-        :file-sync/graph-deleted "Il diagramma attuale è stato eliminato"}
+        :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"}
 
    :tr {:tutorial/text #?(:cljs (rc/inline "tutorial-tr.md")
                                 :default "tutorial-tr.md")
@@ -3542,7 +3542,7 @@
         :settings-page/spell-checker "Yazım denetleyici"
         :settings-page/auto-updater "Otomatik güncelleme"
         :settings-page/disable-sentry "Kullanım verilerini ve tanılamayı Logseq'e gönderin"
-        :settings-page/preferred-outdenting "mantıksal girinti"
+        :settings-page/preferred-outdenting "Mantıksal girinti"
         :settings-page/custom-date-format "Tercih edilen tarih biçimi"
         :settings-page/preferred-file-format "Tercih edilen dosya biçimi"
         :settings-page/preferred-workflow "Tercih edilen iş akışı"
@@ -3551,6 +3551,7 @@
         :settings-page/enable-tooltip "Araç ipuçları"
         :settings-page/enable-journals "Günlük"
         :settings-page/enable-all-pages-public "Yayımlanan tüm sayfaları herkese açık yap"
+        :settings-page/enable-encryption "Şifreleme"
         :settings-page/customize-shortcuts "Klavye kısayolları"
         :settings-page/shortcut-settings "Kısayolları özelleştir"
         :settings-page/home-default-page "Varsayılan ana sayfayı ayarla"

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

@@ -6,11 +6,11 @@
             [cljs-bean.core :as bean]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
-            [frontend.text :as text]))
+            [logseq.graph-parser.text :as text]))
 
 (defn diff
   [s1 s2]
-  (-> ((gobj/get jsdiff "diffLines") s1 s2)
+  (-> ((gobj/get jsdiff "diffLines") s1 s2 (clj->js {"newlineIsToken" true}))
       bean/->clj))
 
 (def inline-special-chars

+ 1 - 1
src/main/frontend/encrypt.cljs

@@ -1,5 +1,5 @@
 (ns frontend.encrypt
-  (:require [frontend.utf8 :as utf8]
+  (:require [logseq.graph-parser.utf8 :as utf8]
             [frontend.db.utils :as db-utils]
             [frontend.db :as db]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/extensions/code.cljs

@@ -132,7 +132,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file :as file-handler]
             [frontend.state :as state]
-            [frontend.utf8 :as utf8]
+            [logseq.graph-parser.utf8 :as utf8]
             [frontend.util :as util]
             [frontend.config :as config]
             [goog.dom :as gdom]

+ 1 - 1
src/main/frontend/extensions/excalidraw.cljs

@@ -149,5 +149,5 @@
     (when-not (and (config/local-db? repo)
                    (not granted?)
                    (not (util/electron?))
-                   (not (mobile-util/is-native-platform?)))
+                   (not (mobile-util/native-platform?)))
       (draw-container option))))

+ 1 - 2
src/main/frontend/extensions/html_parser.cljs

@@ -4,7 +4,6 @@
             [clojure.walk :as walk]
             [frontend.config :as config]
             [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]
             [hickory.core :as hickory]))
 
 (defonce *inside-pre? (atom false))
@@ -75,7 +74,7 @@
                                 :h6 (block-transform 6 children)
                                 :a (let [href (:href attrs)
                                          label (map-join children)
-                                         has-img-tag? (gp-util/safe-re-find #"\[:img" (str x))]
+                                         has-img-tag? (util/safe-re-find #"\[:img" (str x))]
                                      (if has-img-tag?
                                        (export-hiccup x)
                                        (case format

+ 3 - 4
src/main/frontend/extensions/slide.cljs

@@ -1,6 +1,5 @@
 (ns frontend.extensions.slide
   (:require [rum.core :as rum]
-            [medley.core :as medley]
             [cljs-bean.core :as bean]
             [frontend.loader :as loader]
             [frontend.ui :as ui]
@@ -20,11 +19,11 @@
   (let [properties (:block/properties block)]
     (if (seq properties)
       (merge m
-             (medley/map-keys
+             (update-keys
+              properties
               (fn [k]
                 (-> (str "data-" (name k))
-                    (string/replace "data-data-" "data-")))
-              properties))
+                    (string/replace "data-data-" "data-")))))
       m)))
 
 (defonce *loading? (atom false))

+ 1 - 1
src/main/frontend/extensions/video/youtube.cljs

@@ -109,7 +109,7 @@
 (defn gen-youtube-ts-macro []
   (if-let [player (get-player (state/get-input))]
     (util/format "{{youtube-timestamp %s}}" (Math/floor (.getCurrentTime ^js player)))
-    (when (mobile-util/is-native-platform?)
+    (when (mobile-util/native-platform?)
       (notification/show!
        "Please embed a YouTube video at first, then use this icon.
 Remember: You can paste a raw YouTube url as embedded video on mobile."

+ 3 - 4
src/main/frontend/external/roam.cljs

@@ -2,12 +2,11 @@
   (:require [cljs-bean.core :as bean]
             [frontend.external.protocol :as protocol]
             [frontend.date :as date]
-            [medley.core :as medley]
             [clojure.walk :as walk]
             [clojure.string :as string]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
-            [frontend.text :as text]))
+            [logseq.graph-parser.text :as text]))
 
 (defonce all-refed-uids (atom #{}))
 (defonce uid->uuid (atom {}))
@@ -61,7 +60,7 @@
                     (set))]
       (reset! all-refed-uids uids)
       (doseq [uid uids]
-        (swap! uid->uuid assoc uid (medley/random-uuid))))))
+        (swap! uid->uuid assoc uid (random-uuid))))))
 
 (defn transform
   [text]
@@ -76,7 +75,7 @@
 (defn child->text
   [{:keys [uid string children]} level]
   (when-not (and (get @uid->uuid uid) uid)
-    (swap! uid->uuid assoc uid (medley/random-uuid)))
+    (swap! uid->uuid assoc uid (random-uuid)))
   (let [children-text (children->text children (inc level))
         level-pattern (str (apply str (repeat level "\t"))
                            (if (zero? level)

+ 10 - 19
src/main/frontend/format.cljs

@@ -2,30 +2,21 @@
   (:require [frontend.format.mldoc :refer [->MldocMode] :as mldoc]
             [frontend.format.adoc :refer [->AdocMode]]
             [frontend.format.protocol :as protocol]
-            [frontend.text :as text]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]))
 
-(set! mldoc/parse-property text/parse-property)
+;; 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 adoc-record (->AdocMode))
 
-(defn normalize
-  [format]
-  (case (keyword format)
-    :md :markdown
-    :asciidoc :adoc
-    ;; default
-    (keyword format)))
-
-(defn get-format
-  [file]
-  (when file
-    (normalize (keyword (string/lower-case (last (string/split file #"\.")))))))
-
 (defn get-format-record
   [format]
-  (case (normalize format)
+  (case (gp-util/normalize-format format)
     :org
     mldoc-record
     :markdown
@@ -37,9 +28,9 @@
 ;; html
 (defn get-default-config
   ([format]
-   (mldoc/default-config format))
+   (gp-mldoc/default-config format))
   ([format options]
-   (mldoc/default-config format options)))
+   (gp-mldoc/default-config format options)))
 
 (defn to-html
   ([content format]
@@ -49,7 +40,7 @@
      (if (string/blank? content)
        ""
        (if-let [record (get-format-record format)]
-         (protocol/toHtml record content config mldoc/default-references)
+         (protocol/toHtml record content config gp-mldoc/default-references)
          content)))))
 
 (defn to-edn

+ 18 - 653
src/main/frontend/format/block.cljs

@@ -1,665 +1,30 @@
 (ns frontend.format.block
+  "Block code needed by app but not graph-parser"
   (:require [clojure.string :as string]
-            [clojure.walk :as walk]
-            [cljs.core.match :as match]
+            [logseq.graph-parser.block :as gp-block]
             [frontend.config :as config]
-            [frontend.date :as date]
             [frontend.db :as db]
             [frontend.format :as format]
             [frontend.state :as state]
-            [frontend.text :as text]
-            [frontend.utf8 :as utf8]
-            [frontend.util :as util]
-            [frontend.util.property :as property]
-            [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.config :as gp-config]
-            [lambdaisland.glogi :as log]
-            [medley.core :as medley]
-            [frontend.format.mldoc :as mldoc]))
+            [logseq.graph-parser.property :as gp-property]
+            [logseq.graph-parser.mldoc :as gp-mldoc]))
 
-(defn heading-block?
-  [block]
-  (and
-   (vector? block)
-   (= "Heading" (first block))))
-
-(defn get-tag
-  [block]
-  (when-let [tag-value (and (vector? block)
-                            (= "Tag" (first block))
-                            (second block))]
-    (->
-     (map (fn [e]
-            (match/match e
-              ["Plain" s]
-              s
-              ["Link" t]
-              (let [{full_text :full_text} t]
-                full_text)
-              ["Nested_link" t]
-              (let [ {content :content} t]
-                content)
-              :else
-              ""
-              )) tag-value)
-     (string/join))))
-
-(defn get-page-reference
-  [block]
-  (let [page (cond
-               (and (vector? block) (= "Link" (first block)))
-               (let [typ (first (:url (second block)))
-                     value (second (:url (second block)))]
-                 ;; {:url ["File" "file:../pages/hello_world.org"], :label [["Plain" "hello world"]], :title nil}
-                 (or
-                  (and
-                   (= typ "Page_ref")
-                   (and (string? value)
-                        (not (or (gp-config/local-asset? value)
-                                 (gp-config/draw? value))))
-                   value)
-
-                  (and
-                   (= typ "Search")
-                   (text/page-ref? value)
-                   (text/page-ref-un-brackets! value))
-
-                  (and
-                   (= typ "Search")
-                   (not (contains? #{\# \* \/ \[} (first value)))
-                   (let [ext (some-> (util/get-file-ext value) keyword)]
-                     (when (and (not (util/starts-with? value "http:"))
-                                (not (util/starts-with? value "https:"))
-                                (not (util/starts-with? value "file:"))
-                                (not (gp-config/local-asset? value))
-                                (or (= ext :excalidraw)
-                                    (not (contains? (config/supported-formats) ext))))
-                       value)))
-
-                  (and
-                   (= typ "Complex")
-                   (= (:protocol value) "file")
-                   (:link value))
-
-                  (and
-                   (= typ "File")
-                   (second (first (:label (second block)))))))
-
-               (and (vector? block) (= "Nested_link" (first block)))
-               (let [content (:content (last block))]
-                 (subs content 2 (- (count content) 2)))
-
-               (and (vector? block)
-                    (= "Macro" (first block)))
-               (let [{:keys [name arguments]} (second block)
-                     argument (string/join ", " arguments)]
-                   (when (= name "embed")
-                     (text/page-ref-un-brackets! argument)))
-
-               (and (vector? block)
-                    (= "Tag" (first block)))
-               (let [text (get-tag block)]
-                 (text/page-ref-un-brackets! text))
-
-               :else
-               nil)]
-    (text/block-ref-un-brackets! page)))
-
-(defn get-block-reference
-  [block]
-  (when-let [block-id (cond
-                        (and (vector? block)
-                             (= "Block_reference" (first block)))
-                        (last block)
-
-                        (and (vector? block)
-                             (= "Link" (first block))
-                             (map? (second block))
-                             (= "Block_ref" (first (:url (second block)))))
-                        (second (:url (second block)))
-
-                        (and (vector? block)
-                             (= "Macro" (first block)))
-                        (let [{:keys [name arguments]} (second block)]
-                          (when (and (= name "embed")
-                                     (string? (first arguments))
-                                     (string/starts-with? (first arguments) "((")
-                                     (string/ends-with? (first arguments) "))"))
-                            (subs (first arguments) 2 (- (count (first arguments)) 2))))
-
-                        (and (vector? block)
-                             (= "Link" (first block))
-                             (map? (second block)))
-                        (if (= "id" (:protocol (second (:url (second block)))))
-                          (:link (second (:url (second block))))
-                          (let [id (second (:url (second block)))]
-                            (text/block-ref-un-brackets! id)))
-
-                        :else
-                        nil)]
-    (when (and block-id
-               (gp-util/uuid-string? block-id))
-      block-id)))
-
-(defn paragraph-block?
-  [block]
-  (and
-   (vector? block)
-   (= "Paragraph" (first block))))
-
-(defn timestamp-block?
-  [block]
-  (and
-   (vector? block)
-   (= "Timestamp" (first block))))
-
-;; TODO: we should move this to mldoc
-(defn extract-properties
-  [format properties]
-  (when (seq properties)
-    (let [properties (seq properties)
-          page-refs (->>
-                     properties
-                     (remove (fn [[k _]]
-                               (contains? #{:background-color :background_color} (keyword k))))
-                     (map last)
-                     (map (fn [v]
-                            (when (and (string? v)
-                                       (not (mldoc/link? format v)))
-                              (let [v (string/trim v)
-                                    result (text/split-page-refs-without-brackets v {:un-brackets? false})]
-                                (if (coll? result)
-                                  (map text/page-ref-un-brackets! result)
-                                  [])))))
-                     (apply concat)
-                     (remove string/blank?))
-          properties (->> properties
-                          (map (fn [[k v]]
-                                 (let [k (-> (string/lower-case (name k))
-                                             (string/replace " " "-")
-                                             (string/replace "_" "-"))
-                                       k (if (contains? #{"custom_id" "custom-id"} k)
-                                           "id"
-                                           k)
-                                       v (if (coll? v)
-                                           (remove string/blank? v)
-                                           (if (string/blank? v)
-                                             nil
-                                             (text/parse-property format k v)))
-                                       k (keyword k)
-                                       v (if (and
-                                              (string? v)
-                                              (contains? #{:alias :aliases :tags} k))
-                                           (set [v])
-                                           v)
-                                       v (if (coll? v) (set v) v)]
-                                   [k v])))
-                          (remove #(nil? (second %))))]
-      {:properties (into {} properties)
-       :properties-order (map first properties)
-       :page-refs page-refs})))
-
-(defn- paragraph-timestamp-block?
-  [block]
-  (and (paragraph-block? block)
-       (or (timestamp-block? (first (second block)))
-           (timestamp-block? (second (second block))))))
-
-(defn extract-timestamps
-  [block]
-  (some->>
-   (second block)
-   (filter timestamp-block?)
-   (map last)
-   (into {})))
-
-;; {"Deadline" {:date {:year 2020, :month 10, :day 20}, :wday "Tue", :time {:hour 8, :min 0}, :repetition [["DoublePlus"] ["Day"] 1], :active true}}
-(defn timestamps->scheduled-and-deadline
-  [timestamps]
-  (let [timestamps (medley/map-keys (comp keyword string/lower-case) timestamps)
-        m (some->> (select-keys timestamps [:scheduled :deadline])
-                   (map (fn [[k v]]
-                          (let [{:keys [date repetition]} v
-                                {:keys [year month day]} date
-                                day (js/parseInt (str year (util/zero-pad month) (util/zero-pad day)))]
-                            (cond->
-                             (case k
-                               :scheduled
-                               {:scheduled day}
-                               :deadline
-                               {:deadline day})
-                              repetition
-                              (assoc :repeated? true))))))]
-    (apply merge m)))
-
-(defn convert-page-if-journal
-  "Convert journal file name to user' custom date format"
-  [original-page-name]
-  (when original-page-name
-    (let [page-name (util/page-name-sanity-lc original-page-name)
-          day (date/journal-title->int page-name)]
-     (if day
-       (let [original-page-name (date/int->journal-title day)]
-         [original-page-name (util/page-name-sanity-lc original-page-name) day])
-       [original-page-name page-name day]))))
+(defn extract-blocks
+  "Wrapper around logseq.graph-parser.block/extract-blocks that adds in system state"
+  [blocks content with-id? format]
+  (gp-block/extract-blocks blocks content with-id? format
+                           {:user-config (state/get-config)
+                            :block-pattern (config/get-block-pattern format)
+                            :supported-formats (config/supported-formats)
+                            :db (db/get-db (state/get-current-repo))
+                            :date-formatter (state/get-date-formatter)}))
 
 (defn page-name->map
-  "Create a page's map structure given a original page name (string).
-   map as input is supported for legacy compatibility.
-   with-timestamp?: assign timestampes to the map structure.
-    Useful when creating new pages from references or namespaces,
-    as there's no chance to introduce timestamps via editing in page"
+  "Wrapper around logseq.graph-parser.block/page-name->map that adds in db"
   ([original-page-name with-id?]
    (page-name->map original-page-name with-id? true))
   ([original-page-name with-id? with-timestamp?]
-   (cond
-     (and original-page-name (string? original-page-name))
-     (let [original-page-name (util/remove-boundary-slashes original-page-name)
-           [original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
-           namespace? (and (not (boolean (text/get-nested-page-name original-page-name)))
-                           (text/namespace-page? original-page-name))
-           page-entity (db/entity [:block/name page-name])
-           original-page-name (or (:block/original-name page-entity) original-page-name)]
-       (merge
-        {:block/name page-name
-         :block/original-name original-page-name}
-        (when with-id?
-          (if page-entity
-            {:block/uuid (:block/uuid page-entity)}
-            {:block/uuid (db/new-block-id)}))
-        (when namespace?
-          (let [namespace (first (gp-util/split-last "/" original-page-name))]
-            (when-not (string/blank? namespace)
-              {:block/namespace {:block/name (util/page-name-sanity-lc namespace)}})))
-        (when (and with-timestamp? (not page-entity)) ;; Only assign timestamp on creating new entity
-          (let [current-ms (util/time-ms)]
-            {:block/created-at current-ms
-             :block/updated-at current-ms}))
-        (if journal-day
-          {:block/journal? true
-           :block/journal-day journal-day}
-          {:block/journal? false})))
-
-     (and (map? original-page-name) (:block/uuid original-page-name))
-     original-page-name
-
-     (and (map? original-page-name) with-id?)
-     (assoc original-page-name :block/uuid (db/new-block-id))
-
-     :else
-     nil)))
-
-(defn with-page-refs
-  [{:keys [title body tags refs marker priority] :as block} with-id?]
-  (let [refs (->> (concat tags refs [marker priority])
-                  (remove string/blank?)
-                  (distinct))
-        refs (atom refs)]
-    (walk/prewalk
-     (fn [form]
-       ;; skip custom queries
-       (when-not (and (vector? form)
-                      (= (first form) "Custom")
-                      (= (second form) "query"))
-         (when-let [page (get-page-reference form)]
-           (swap! refs conj page))
-         (when-let [tag (get-tag form)]
-           (let [tag (text/page-ref-un-brackets! tag)]
-             (when (gp-util/tag-valid? tag)
-               (swap! refs conj tag))))
-         form))
-     (concat title body))
-    (let [refs (remove string/blank? @refs)
-          children-pages (->> (mapcat (fn [p]
-                                        (let [p (if (map? p)
-                                                  (:block/original-name p)
-                                                  p)]
-                                          (when (string? p)
-                                            (let [p (or (text/get-nested-page-name p) p)]
-                                              (when (text/namespace-page? p)
-                                                (util/split-namespace-pages p))))))
-                                      refs)
-                              (remove string/blank?)
-                              (distinct))
-          refs (->> (distinct (concat refs children-pages))
-                    (remove nil?))
-          refs (map (fn [ref] (page-name->map ref with-id?)) refs)]
-      (assoc block :refs refs))))
-
-(defn with-block-refs
-  [{:keys [title body] :as block}]
-  (let [ref-blocks (atom nil)]
-    (walk/postwalk
-     (fn [form]
-       (when-let [block (get-block-reference form)]
-         (swap! ref-blocks conj block))
-       form)
-     (concat title body))
-    (let [ref-blocks (->> @ref-blocks
-                          (filter gp-util/uuid-string?))
-          ref-blocks (map
-                       (fn [id]
-                         [:block/uuid (medley/uuid id)])
-                       ref-blocks)
-          refs (distinct (concat (:refs block) ref-blocks))]
-      (assoc block :refs refs))))
-
-(defn- block-keywordize
-  [block]
-  (medley/map-keys
-   (fn [k]
-     (if (namespace k)
-       k
-       (keyword "block" k)))
-   block))
-
-(defn- sanity-blocks-data
-  [blocks]
-  (map (fn [block]
-         (if (map? block)
-           (block-keywordize (gp-util/remove-nils block))
-           block))
-       blocks))
-
-(defn with-path-refs
-  [blocks]
-  (loop [blocks blocks
-         acc []
-         parents []]
-    (if (empty? blocks)
-      acc
-      (let [block (first blocks)
-            cur-level (:block/level block)
-            level-diff (- cur-level
-                          (get (last parents) :block/level 0))
-            [path-refs parents]
-            (cond
-              (zero? level-diff)            ; sibling
-              (let [path-refs (mapcat :block/refs (drop-last parents))
-                    parents (conj (vec (butlast parents)) block)]
-                [path-refs parents])
-
-              (> level-diff 0)              ; child
-              (let [path-refs (mapcat :block/refs parents)]
-                [path-refs (conj parents block)])
-
-              (< level-diff 0)              ; new parent
-              (let [parents (vec (take-while (fn [p] (< (:block/level p) cur-level)) parents))
-                    path-refs (mapcat :block/refs parents)]
-                [path-refs (conj parents block)]))
-            path-ref-pages (->> path-refs
-                                (concat (:block/refs block))
-                                (map (fn [ref]
-                                       (cond
-                                         (map? ref)
-                                         (:block/name ref)
-
-                                         :else
-                                         ref)))
-                                (remove string/blank?)
-                                (map (fn [ref]
-                                       (if (string? ref)
-                                         {:block/name (util/page-name-sanity-lc ref)}
-                                         ref)))
-                                (remove vector?)
-                                (remove nil?)
-                                (distinct))]
-        (recur (rest blocks)
-               (conj acc (assoc block :block/path-refs path-ref-pages))
-               parents)))))
-
-(defn block-tags->pages
-  [{:keys [tags] :as block}]
-  (if (seq tags)
-    (assoc block :tags (map (fn [tag]
-                              (let [tag (text/page-ref-un-brackets! tag)]
-                                [:block/name (util/page-name-sanity-lc tag)])) tags))
-    block))
-
-(defn- get-block-content
-  [utf8-content block format meta]
-  (let [content (if-let [end-pos (:end_pos meta)]
-                  (utf8/substring utf8-content
-                                  (:start_pos meta)
-                                  end-pos)
-                  (utf8/substring utf8-content
-                                  (:start_pos meta)))
-        content (when content
-                  (let [content (text/remove-level-spaces content format)]
-                    (if (or (:pre-block? block)
-                            (= (:format block) :org))
-                      content
-                      (mldoc/remove-indentation-spaces content (inc (:level block)) false))))]
-    (if (= format :org)
-      content
-      (property/->new-properties content))))
-
-(defn get-custom-id-or-new-id
-  [properties]
-  (or (when-let [custom-id (or (get-in properties [:properties :custom-id])
-                               (get-in properties [:properties :custom_id])
-                               (get-in properties [:properties :id]))]
-        (let [custom-id (and (string? custom-id) (string/trim custom-id))]
-          (when (and custom-id (gp-util/uuid-string? custom-id))
-            (uuid custom-id))))
-      (db/new-block-id)))
-
-(defn get-page-refs-from-properties
-  [properties]
-  (let [page-refs (mapcat (fn [v] (cond
-                                   (coll? v)
-                                   v
-
-                                   (text/page-ref? v)
-                                   [(text/page-ref-un-brackets! v)]
-
-                                   :else
-                                   nil)) (vals properties))
-        page-refs (remove string/blank? page-refs)]
-    (map (fn [page] (page-name->map page true)) page-refs)))
-
-(defn with-page-block-refs
-  [block with-id?]
-  (some-> block
-          (with-page-refs with-id?)
-          with-block-refs
-          block-tags->pages
-          (update :refs (fn [col] (remove nil? col)))))
-
-(defn with-pre-block-if-exists
-  [blocks body pre-block-properties encoded-content]
-  (let [first-block (first blocks)
-        first-block-start-pos (get-in first-block [:block/meta :start_pos])
-
-        ;; Add pre-block
-        blocks (if (or (> first-block-start-pos 0)
-                       (empty? blocks))
-                 (cons
-                  (merge
-                   (let [content (utf8/substring encoded-content 0 first-block-start-pos)
-                         {:keys [properties properties-order]} pre-block-properties
-                         id (get-custom-id-or-new-id {:properties properties})
-                         property-refs (->> (get-page-refs-from-properties properties)
-                                            (map :block/original-name))
-                         block {:uuid id
-                                :content content
-                                :level 1
-                                :properties properties
-                                :properties-order properties-order
-                                :refs property-refs
-                                :pre-block? true
-                                :unordered true
-                                :body body}
-                         block (with-page-block-refs block false)]
-                     (block-keywordize block))
-                   (select-keys first-block [:block/format :block/page]))
-                  blocks)
-                 blocks)]
-    (with-path-refs blocks)))
-
-(defn- construct-block
-  [block properties timestamps body encoded-content format pos-meta with-id?]
-  (let [id (get-custom-id-or-new-id properties)
-        ref-pages-in-properties (->> (:page-refs properties)
-                                     (remove string/blank?))
-        block (second block)
-        unordered? (:unordered block)
-        markdown-heading? (and (:size block) (= :markdown format))
-        block (if markdown-heading?
-                (assoc block
-                       :type :heading
-                       :level (if unordered? (:level block) 1)
-                       :heading-level (or (:size block) 6))
-                block)
-        block (cond->
-                (assoc block
-                       :uuid id
-                       :refs ref-pages-in-properties
-                       :format format
-                       :meta pos-meta)
-                (seq (:properties properties))
-                (assoc :properties (:properties properties))
-
-                (seq (:properties-order properties))
-                (assoc :properties-order (:properties-order properties)))
-        block (if (get-in block [:properties :collapsed])
-                (assoc block :collapsed? true)
-                block)
-        block (assoc block
-                     :content (get-block-content encoded-content block format pos-meta))
-        block (if (seq timestamps)
-                (merge block (timestamps->scheduled-and-deadline timestamps))
-                block)
-        block (assoc block :body body)
-        block (with-page-block-refs block with-id?)
-        {:keys [created-at updated-at]} (:properties properties)
-        block (cond-> block
-                (and created-at (integer? created-at))
-                (assoc :block/created-at created-at)
-
-                (and updated-at (integer? updated-at))
-                (assoc :block/updated-at updated-at))]
-    (dissoc block :title :body :anchor)))
-
-(defn extract-blocks
-  "Extract headings from mldoc ast.
-  Args:
-    `blocks`: mldoc ast.
-    `content`: markdown or org-mode text.
-    `with-id?`: If `with-id?` equals to true, all the referenced pages will have new db ids.
-    `format`: content's format, it could be either :markdown or :org-mode."
-  [blocks content with-id? format]
-  {:pre [(seq blocks) (string? content) (boolean? with-id?) (contains? #{:markdown :org} format)]}
-  (try
-    (let [encoded-content (utf8/encode content)
-          [blocks body pre-block-properties]
-          (loop [headings []
-                 blocks (reverse blocks)
-                 timestamps {}
-                 properties {}
-                 body []]
-            (if (seq blocks)
-              (let [[block pos-meta] (first blocks)
-                    ;; fix start_pos
-                    pos-meta (assoc pos-meta :end_pos
-                                    (if (seq headings)
-                                      (get-in (last headings) [:meta :start_pos])
-                                      nil))]
-                (cond
-                  (paragraph-timestamp-block? block)
-                  (let [timestamps (extract-timestamps block)
-                        timestamps' (merge timestamps timestamps)]
-                    (recur headings (rest blocks) timestamps' properties body))
-
-                  (property/properties-ast? block)
-                  (let [properties (extract-properties format (second block))]
-                    (recur headings (rest blocks) timestamps properties body))
-
-                  (heading-block? block)
-                  (let [block (construct-block block properties timestamps body encoded-content format pos-meta with-id?)]
-                    (recur (conj headings block) (rest blocks) {} {} []))
-
-                  :else
-                  (recur headings (rest blocks) timestamps properties (conj body block))))
-              [(-> (reverse headings)
-                   sanity-blocks-data)
-               body
-               properties]))
-          result (with-pre-block-if-exists blocks body pre-block-properties encoded-content)]
-      (map #(dissoc % :block/meta) result))
-    (catch js/Error e
-      (js/console.error "extract-blocks-failed")
-      (log/error :exception e))))
-
-(defn with-parent-and-left
-  [page-id blocks]
-  (loop [blocks (map (fn [block] (assoc block :block/level-spaces (:block/level block))) blocks)
-         parents [{:page/id page-id     ; db id or a map {:block/name "xxx"}
-                   :block/level 0
-                   :block/level-spaces 0}]
-         result []]
-    (if (empty? blocks)
-      (map #(dissoc % :block/level-spaces) result)
-      (let [[block & others] blocks
-            level-spaces (:block/level-spaces block)
-            {:block/keys [uuid level parent] :as last-parent} (last parents)
-            parent-spaces (:block/level-spaces last-parent)
-            [blocks parents result]
-            (cond
-              (= level-spaces parent-spaces)        ; sibling
-              (let [block (assoc block
-                                 :block/parent parent
-                                 :block/left [:block/uuid uuid]
-                                 :block/level level)
-                    parents' (conj (vec (butlast parents)) block)
-                    result' (conj result block)]
-                [others parents' result'])
-
-              (> level-spaces parent-spaces)         ; child
-              (let [parent (if uuid [:block/uuid uuid] (:page/id last-parent))
-                    block (cond->
-                            (assoc block
-                                  :block/parent parent
-                                  :block/left parent)
-                            ;; fix block levels with wrong order
-                            ;; For example:
-                            ;;   - a
-                            ;; - b
-                            ;; What if the input indentation is two spaces instead of 4 spaces
-                            (>= (- level-spaces parent-spaces) 1)
-                            (assoc :block/level (inc level)))
-                    parents' (conj parents block)
-                    result' (conj result block)]
-                [others parents' result'])
-
-              (< level-spaces parent-spaces)
-              (cond
-                (some #(= (:block/level-spaces %) (:block/level-spaces block)) parents) ; outdent
-                (let [parents' (vec (filter (fn [p] (<= (:block/level-spaces p) level-spaces)) parents))
-                      left (last parents')
-                      blocks (cons (assoc (first blocks)
-                                          :block/level (dec level)
-                                          :block/left [:block/uuid (:block/uuid left)])
-                                   (rest blocks))]
-                  [blocks parents' result])
-
-                :else
-                (let [[f r] (split-with (fn [p] (<= (:block/level-spaces p) level-spaces)) parents)
-                      left (first r)
-                      parent-id (if-let [block-id (:block/uuid (last f))]
-                                  [:block/uuid block-id]
-                                  page-id)
-                      block (cond->
-                              (assoc block
-                                     :block/parent parent-id
-                                     :block/left [:block/uuid (:block/uuid left)]
-                                     :block/level (:block/level left)
-                                     :block/level-spaces (:block/level-spaces left)))
-
-                      parents' (->> (concat f [block]) vec)
-                      result' (conj result block)]
-                  [others parents' result'])))]
-        (recur blocks parents result)))))
+   (gp-block/page-name->map original-page-name with-id? (db/get-db (state/get-current-repo)) with-timestamp? (state/get-date-formatter))))
 
 (defn parse-block
   ([block]
@@ -702,12 +67,12 @@
                        (str (config/get-block-pattern format) " " (string/triml content)))]
        (if-let [result (state/get-block-ast block-uuid content)]
          result
-         (let [ast (->> (format/to-edn content format (mldoc/default-config format))
+         (let [ast (->> (format/to-edn content format (gp-mldoc/default-config format))
                         (map first))
-               title (when (heading-block? (first ast))
+               title (when (gp-block/heading-block? (first ast))
                        (:title (second (first ast))))
                body (vec (if title (rest ast) ast))
-               body (drop-while property/properties-ast? body)
+               body (drop-while gp-property/properties-ast? body)
                result (cond->
                         (if (seq body) {:block/body body} {})
                         title

+ 14 - 209
src/main/frontend/format/mldoc.cljs

@@ -1,68 +1,19 @@
 (ns frontend.format.mldoc
-  (:require [cljs-bean.core :as bean]
-            [clojure.string :as string]
+  "Mldoc code needed by app but not graph-parser"
+  (:require [clojure.string :as string]
             [frontend.format.protocol :as protocol]
-            [frontend.utf8 :as utf8]
+            [frontend.state :as state]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [medley.core :as medley]
             ["mldoc" :as mldoc :refer [Mldoc]]
-            [linked.core :as linked]
-            [logseq.graph-parser.util :as gp-util]
-            [logseq.graph-parser.config :as gp-config]))
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.util :as gp-util]))
 
-(defonce parseJson (gobj/get Mldoc "parseJson"))
-(defonce parseInlineJson (gobj/get Mldoc "parseInlineJson"))
-(defonce parseOPML (gobj/get Mldoc "parseOPML"))
-(defonce export (gobj/get Mldoc "export"))
 (defonce anchorLink (gobj/get Mldoc "anchorLink"))
+(defonce parseOPML (gobj/get Mldoc "parseOPML"))
 (defonce parseAndExportMarkdown (gobj/get Mldoc "parseAndExportMarkdown"))
 (defonce parseAndExportOPML (gobj/get Mldoc "parseAndExportOPML"))
-(defonce astExportMarkdown (gobj/get Mldoc "astExportMarkdown"))
-
-(defn convert-export-md-remove-options [opts]
-  (->>
-   (mapv (fn [opt]
-             (case opt
-               :page-ref ["Page_ref"]
-               :emphasis ["Emphasis"]
-               []))
-         opts)
-   (remove empty?)))
-
-
-(defn default-config
-  ([format]
-   (default-config format {:export-heading-to-list? false}))
-  ([format {:keys [export-heading-to-list? export-keep-properties? export-md-indent-style export-md-remove-options parse_outline_only?]}]
-   (let [format (string/capitalize (name (or format :markdown)))]
-     (->> {:toc false
-           :parse_outline_only (or parse_outline_only? false)
-           :heading_number false
-           :keep_line_break true
-           :format format
-           :heading_to_list (or export-heading-to-list? false)
-           :exporting_keep_properties export-keep-properties?
-           :export_md_indent_style export-md-indent-style
-           :export_md_remove_options
-           (convert-export-md-remove-options export-md-remove-options)}
-          (filter #(not(nil? (second %))))
-          (into {})
-          (bean/->js)
-          (js/JSON.stringify)))))
-
-(def default-references
-  (js/JSON.stringify
-   (clj->js {:embed_blocks []
-             :embed_pages []})))
-
-(defn parse-json
-  [content config]
-  (parseJson content config))
-
-(defn inline-parse-json
-  [text config]
-  (parseInlineJson text config))
+(defonce export (gobj/get Mldoc "export"))
 
 (defn parse-opml
   [content]
@@ -72,118 +23,14 @@
   [content config references]
   (parseAndExportMarkdown content
                           config
-                          (or references default-references)))
+                          (or references gp-mldoc/default-references)))
 
 (defn parse-export-opml
   [content config title references]
   (parseAndExportOPML content
                       config
                       title
-                      (or references default-references)))
-
-(defn ast-export-markdown
-  [ast config references]
-  (astExportMarkdown ast
-                     config
-                     (or references default-references)))
-
-(defn remove-indentation-spaces
-  [s level remove-first-line?]
-  (let [lines (string/split-lines s)
-        [f & r] lines
-        body (map (fn [line]
-                    (if (string/blank? (gp-util/safe-subs line 0 level))
-                      (gp-util/safe-subs line level)
-                      line))
-               (if remove-first-line? lines r))
-        content (if remove-first-line? body (cons f body))]
-    (string/join "\n" content)))
-
-(defn- ->vec
-  [s]
-  (if (string? s) [s] s))
-
-(defn- ->vec-concat
-  [& coll]
-  (->> (map ->vec coll)
-       (remove nil?)
-       (apply concat)
-       (distinct)))
-
-(defn collect-page-properties
-  [ast parse-property]
-  (if (seq ast)
-    (let [original-ast ast
-          ast (map first ast)           ; without position meta
-          directive? (fn [[item _]] (= "directive" (string/lower-case (first item))))
-          grouped-ast (group-by directive? original-ast)
-          directive-ast (take-while directive? original-ast)
-          [properties-ast other-ast] (if (= "Property_Drawer" (ffirst ast))
-                                       [(last (first ast))
-                                        (rest original-ast)]
-                                       [(->> (map first directive-ast)
-                                             (map rest))
-                                        (get grouped-ast false)])
-          properties (->>
-                      properties-ast
-                      (map (fn [[k v]]
-                             (let [k (keyword (string/lower-case k))
-                                   v (if (contains? #{:title :description :filters :macro} k)
-                                       v
-                                       (parse-property k v))]
-                               [k v]))))
-          properties (into (linked/map) properties)
-          macro-properties (filter (fn [x] (= :macro (first x))) properties)
-          macros (if (seq macro-properties)
-                   (->>
-                    (map
-                     (fn [[_ v]]
-                       (let [[k v] (gp-util/split-first " " v)]
-                         (mapv
-                          string/trim
-                          [k v])))
-                     macro-properties)
-                    (into {}))
-                   {})
-          properties (->> (remove (fn [x] (= :macro (first x))) properties)
-                          (into (linked/map)))
-          properties (cond-> properties
-                       (seq macros)
-                       (assoc :macros macros))
-          alias (:alias properties)
-          alias (when alias
-                  (if (coll? alias)
-                    (remove string/blank? alias)
-                    [alias]))
-          filetags (when-let [org-file-tags (:filetags properties)]
-                     (->> (string/split org-file-tags ":")
-                          (remove string/blank?)))
-          tags (:tags properties)
-          tags (->> (->vec-concat tags filetags)
-                    (remove string/blank?))
-          properties (assoc properties :tags tags :alias alias)
-          properties (-> properties
-                         (update :filetags (constantly filetags)))
-          properties (medley/remove-kv (fn [_k v] (or (nil? v) (and (coll? v) (empty? v)))) properties)]
-      (if (seq properties)
-        (cons [["Properties" properties] nil] other-ast)
-        original-ast))
-    ast))
-
-(defn update-src-full-content
-  [ast content]
-  (let [content (utf8/encode content)]
-    (map (fn [[block pos-meta]]
-          (if (and (vector? block)
-                   (= "Src" (first block)))
-            (let [{:keys [start_pos end_pos]} pos-meta
-                  content (utf8/substring content start_pos end_pos)
-                  spaces (re-find #"^[\t ]+" (first (string/split-lines content)))
-                  content (if spaces (remove-indentation-spaces content (count spaces) true)
-                              content)
-                  block ["Src" (assoc (second block) :full_content content)]]
-              [block pos-meta])
-            [block pos-meta])) ast)))
+                      (or references gp-mldoc/default-references)))
 
 (defn block-with-title?
   [type]
@@ -192,45 +39,21 @@
                "Hiccup"
                "Heading"} type))
 
-(def parse-property nil)
-
-(defn ->edn
-  [content config]
-  (if (string? content)
-    (try
-      (if (string/blank? content)
-        []
-        (-> content
-            (parse-json config)
-            (gp-util/json->clj)
-            (update-src-full-content content)
-            (collect-page-properties parse-property)))
-      (catch js/Error e
-        (js/console.error e)
-        []))
-    (log/error :edn/wrong-content-type content)))
-
 (defn opml->edn
   [content]
   (try
     (if (string/blank? content)
       {}
       (let [[headers blocks] (-> content (parse-opml) (gp-util/json->clj))]
-        [headers (collect-page-properties blocks parse-property)]))
+        [headers (gp-mldoc/collect-page-properties blocks gp-mldoc/parse-property (state/get-config))]))
     (catch js/Error e
       (log/error :edn/convert-failed e)
       [])))
 
-(defn inline->edn
-  [text config]
-  (try
-    (if (string/blank? text)
-      {}
-      (-> text
-          (inline-parse-json config)
-          (gp-util/json->clj)))
-    (catch js/Error _e
-      [])))
+(defn ->edn
+  "Wrapper around gp-mldoc/->edn which provides config state"
+  [content config]
+  (gp-mldoc/->edn content config (state/get-config)))
 
 (defrecord MldocMode []
   protocol/Format
@@ -259,21 +82,3 @@
   [ast typ]
   (and (contains? #{"Drawer"} (ffirst ast))
        (= typ (second (first ast)))))
-
-(defn link?
-  [format link]
-  (when (string? link)
-    (let [[type link] (first (inline->edn link (default-config format)))
-          [ref-type ref-value] (:url link)]
-      (and (= "Link" type)
-           (or
-            ;; 1. url
-            (not (contains? #{"Page_ref" "Block_ref"} ref-type))
-
-            (and (contains? #{"Page_ref"} ref-type)
-                 (or
-                  ;; 2. excalidraw link
-                  (gp-config/draw? ref-value)
-
-                  ;; 3. local asset link
-                  (boolean (gp-config/local-asset? ref-value)))))))))

+ 5 - 5
src/main/frontend/fs.cljs

@@ -32,7 +32,7 @@
       (and (util/electron?) (not bfs-local?))
       node-record
 
-      (mobile-util/is-native-platform?)
+      (mobile-util/native-platform?)
       mobile-record
 
       (local-db? dir)
@@ -109,7 +109,7 @@
 
     :else
     (let [[old-path new-path]
-          (map #(if (or (util/electron?) (mobile-util/is-native-platform?))
+          (map #(if (or (util/electron?) (mobile-util/native-platform?))
                   %
                   (str (config/get-repo-dir repo) "/" %))
                [old-path new-path])]
@@ -125,7 +125,7 @@
     (util/electron?)
     node-record
 
-    (mobile-util/is-native-platform?)
+    (mobile-util/native-platform?)
     mobile-record
 
     :else
@@ -136,7 +136,7 @@
   (let [record (get-record)]
     (p/let [result (protocol/open-dir record ok-handler)]
       (if (or (util/electron?)
-              (mobile-util/is-native-platform?))
+              (mobile-util/native-platform?))
         (let [[dir & paths] (bean/->clj result)]
           [(:path dir) paths])
         result))))
@@ -145,7 +145,7 @@
   [path-or-handle ok-handler]
   (let [record (get-record)
         electron? (util/electron?)
-        mobile? (mobile-util/is-native-platform?)]
+        mobile? (mobile-util/native-platform?)]
     (p/let [result (protocol/get-files record path-or-handle ok-handler)]
       (if (or electron? mobile?)
         (let [result (bean/->clj result)]

+ 125 - 53
src/main/frontend/fs/sync.cljs

@@ -18,6 +18,9 @@
             [frontend.util.persist-var :as persist-var]
             [frontend.handler.notification :as notification]
             [frontend.context.i18n :refer [t]]
+            [frontend.diff :as diff]
+            [frontend.db :as db]
+            [frontend.fs :as fs]
             [medley.core :refer [dedupe-by]]
             [rum.core :as rum]))
 
@@ -52,7 +55,12 @@
 ;;       and re-produce a new same-file-delete diff.
 
 ;;; ### specs
-(s/def ::state #{::idle
+(s/def ::state #{;; do following jobs when ::starting:
+                 ;; - wait seconds for file-change-events from file-watcher
+                 ;; - drop redundant file-change-events
+                 ;; - setup states in `frontend.state`
+                 ::starting
+                 ::idle
                  ;; sync local-changed files
                  ::local->remote
                  ;; sync remote latest-transactions
@@ -157,7 +165,7 @@
                                          (offer! remote-changes-chan data)))))))
 
 (defn ws-listen!
-  "return channal which output messages from server"
+  "return channel which output messages from server"
   [graph-uuid *ws]
   (let [remote-changes-chan (chan (async/sliding-buffer 1))]
     (ws-listen!* graph-uuid *ws remote-changes-chan)
@@ -345,7 +353,7 @@
    :TXType "update_files"
    :TXContent (string/join "/" [user-uuid graph-uuid relative-path])})
 
-(defn- filepaths->partitioned-filetxns
+(defn filepaths->partitioned-filetxns
   "transducer.
   1. filepaths -> diff
   2. diffs->partitioned-filetxns"
@@ -356,6 +364,7 @@
    (map-indexed filepath->diff)
    (diffs->partitioned-filetxns n)))
 
+
 (deftype FileMetadata [size etag path last-modified remote? ^:mutable normalized-path]
   Object
   (get-normalized-path [_]
@@ -382,7 +391,7 @@
   (-pr-writer [_ w _opts]
     (write-all w (str {:size size :etag etag :path path :remote? remote?}))))
 
-(defn- relative-path [o]
+(defn relative-path [o]
   (cond
     (implements? IRelativePath o)
     (-relative-path o)
@@ -716,23 +725,68 @@
 
 (def remoteapi (->RemoteAPI))
 
+(defn- add-new-version-file
+  [repo path content]
+  (go
+    (println "add-new-version-file: "
+             (<! (p->c (ipc/ipc "addVersionFile" (config/get-local-dir repo) path content))))))
+
+(defn- is-journals-or-pages?
+  [filetxn]
+  (let [rel-path (relative-path filetxn)]
+    (or (string/starts-with? rel-path "journals/")
+        (string/starts-with? rel-path "pages/"))))
+
+(defn- need-add-version-file?
+  "when we need to create a new version file:
+  1. when apply a 'update' filetxn, it already exists(same page name) locally and has delete diffs
+  2. when apply a 'delete' filetxn, its origin remote content and local content are different
+     - TODO: we need to store origin remote content md5 in server db
+  3. create version files only for files under 'journals/', 'pages/' dir"
+  [^FileTxn filetxn origin-db-content]
+  (go
+    (cond
+      (.renamed? filetxn)
+      false
+      (.-deleted? filetxn)
+      false
+      (.-updated? filetxn)
+      (let [path (relative-path filetxn)
+            repo (state/get-current-repo)
+            file-path (config/get-file-path repo path)
+            content (<! (p->c (fs/read-file "" file-path)))]
+        (and origin-db-content
+             (or (nil? content)
+                 (some :removed (diff/diff origin-db-content content))))))))
+
 (defn- apply-filetxns
   [graph-uuid base-path filetxns]
-  (cond
-    (.renamed? (first filetxns))
-    (let [filetxn (first filetxns)]
-      (assert (= 1 (count filetxns)))
-      (rename-local-file rsapi graph-uuid base-path
-                         (relative-path (.-from-path filetxn))
-                         (relative-path (.-to-path filetxn))))
-
-    (.-updated? (first filetxns))
-    (update-local-files rsapi graph-uuid base-path (map relative-path filetxns))
-
-    (.-deleted? (first filetxns))
-    (let [filetxn (first filetxns)]
-      (assert (= 1 (count filetxns)))
-      (go
+  (go
+    (cond
+      (.renamed? (first filetxns))
+      (let [^FileTxn filetxn (first filetxns)
+            from-path (.-from-path filetxn)
+            to-path (.-to-path filetxn)]
+        (assert (= 1 (count filetxns)))
+        (<! (rename-local-file rsapi graph-uuid base-path
+                               (relative-path from-path)
+                               (relative-path to-path))))
+
+      (.-updated? (first filetxns))
+      (let [repo (state/get-current-repo)
+            txn->db-content-vec (->> filetxns
+                                     (mapv
+                                      #(when (is-journals-or-pages? %)
+                                         [% (db/get-file repo (config/get-file-path repo (relative-path %)))]))
+                                     (remove nil?))]
+        (<! (update-local-files rsapi graph-uuid base-path (map relative-path filetxns)))
+        (doseq [[filetxn origin-db-content] txn->db-content-vec]
+          (when (need-add-version-file? filetxn origin-db-content)
+            (add-new-version-file repo (relative-path filetxn) origin-db-content))))
+
+      (.-deleted? (first filetxns))
+      (let [filetxn (first filetxns)]
+        (assert (= 1 (count filetxns)))
         (let [r (<! (delete-local-files rsapi graph-uuid base-path [(relative-path filetxn)]))]
           (if (and (instance? ExceptionInfo r)
                    (string/index-of (str (ex-cause r)) "No such file or directory"))
@@ -745,21 +799,23 @@
          sync-state--remove-current-remote->local-files
          sync-state--stopped?)
 
-(defn- apply-filetxns-partitions
+(defn apply-filetxns-partitions
   "won't call update-graph-txid! when *txid is nil"
-  [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped]
+  [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped before-f after-f]
   (go-loop [filetxns-partitions* filetxns-partitions]
     (if @*stopped
       {:stop true}
       (when (seq filetxns-partitions*)
         (let [filetxns (first filetxns-partitions*)
               paths (map relative-path filetxns)
-              _ (swap! *sync-state sync-state--add-current-remote->local-files paths)
+              _ (when (and before-f (fn? before-f)) (before-f filetxns))
+              _ (when *sync-state (swap! *sync-state sync-state--add-current-remote->local-files paths))
               r (<! (apply-filetxns graph-uuid base-path filetxns))
-              _ (swap! *sync-state sync-state--remove-current-remote->local-files paths)]
+              _ (when *sync-state (swap! *sync-state sync-state--remove-current-remote->local-files paths))]
           (if (instance? ExceptionInfo r)
             r
             (let [latest-txid (apply max (map #(.-txid ^FileTxn %) filetxns))]
+              (when (and after-f (fn? after-f)) (after-f filetxns))
               (when *txid
                 (reset! *txid latest-txid)
                 (update-graphs-txid! latest-txid graph-uuid user-uuid repo))
@@ -847,7 +903,7 @@
   "create a new sync-state"
   []
   {:post [(s/valid? ::sync-state %)]}
-  {:state ::idle
+  {:state ::starting
    :current-local->remote-files #{}
    :current-remote->local-files #{}
    :queued-local->remote-files #{}
@@ -923,32 +979,32 @@
   if local-txid != remote-txid, return {:need-sync-remote true}"))
 
 (defrecord Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state
-                              ^:mutable local->remote-syncer *stopped]
+                                ^:mutable local->remote-syncer *stopped]
   Object
   (set-local->remote-syncer! [_ s] (set! local->remote-syncer s))
   (sync-files-remote->local!
     [_ relative-filepaths latest-txid]
     (go
       (let [partitioned-filetxns
-              (sequence (filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
-                        relative-filepaths)
-              r
-              (if (empty? (flatten partitioned-filetxns))
-                {:succ true}
-                (<! (apply-filetxns-partitions
-                     *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
-                     nil *stopped)))]
-          (cond
-            (instance? ExceptionInfo r)
-            {:unknown r}
+            (sequence (filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
+                      relative-filepaths)
+            r
+            (if (empty? (flatten partitioned-filetxns))
+              {:succ true}
+              (<! (apply-filetxns-partitions
+                   *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo
+                   nil *stopped nil nil)))]
+        (cond
+          (instance? ExceptionInfo r)
+          {:unknown r}
 
-            @*stopped
-            {:stop true}
+          @*stopped
+          {:stop true}
 
-            :else
-            (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
-                (reset! *txid latest-txid)
-                {:succ true})))))
+          :else
+          (do (update-graphs-txid! latest-txid graph-uuid user-uuid repo)
+              (reset! *txid latest-txid)
+              {:succ true})))))
 
   IRemote->LocalSync
   (stop-remote->local! [_] (vreset! *stopped true))
@@ -977,7 +1033,8 @@
                               (reset! *txid latest-txid)
                               {:succ true})
                           (<! (apply-filetxns-partitions
-                               *sync-state user-uuid graph-uuid base-path partitioned-filetxns repo *txid *stopped)))))))))]
+                               *sync-state user-uuid graph-uuid base-path
+                               partitioned-filetxns repo *txid *stopped nil nil)))))))))]
         (cond
           (instance? ExceptionInfo r)
           {:unknown r}
@@ -998,12 +1055,11 @@
             remote-all-files-meta (<! remote-all-files-meta-c)
             local-all-files-meta (<! local-all-files-meta-c)
             diff-remote-files (set/difference remote-all-files-meta local-all-files-meta)
-            latest-txid (:TXId
-                         (<! (get-remote-graph remoteapi nil graph-uuid)))]
+            latest-txid (:TXId (<! (get-remote-graph remoteapi nil graph-uuid)))]
         (println "[full-sync(remote->local)]"
                  (count diff-remote-files) "files need to sync")
         (<! (.sync-files-remote->local!
-             this (map -relative-path diff-remote-files)
+             this (map relative-path diff-remote-files)
              latest-txid))))))
 
 (defn- file-changed?
@@ -1387,10 +1443,16 @@
 
 (defn- check-graph-belong-to-current-user
   [current-user-uuid graph-user-uuid]
-  (let [result (= current-user-uuid graph-user-uuid)]
-    (when-not result
-      (notification/show! (t :file-sync/other-user-graph) :warning false))
-    result))
+  (cond
+    (nil? current-user-uuid)
+    false
+
+    (= current-user-uuid graph-user-uuid)
+    true
+
+    :else
+    (do (notification/show! (t :file-sync/other-user-graph) :warning false)
+        false)))
 
 (defn check-remote-graph-exists
   [local-graph-uuid]
@@ -1417,24 +1479,34 @@
       ;; 1. if remote graph has been deleted, clear graphs-txid.edn
       ;; 2. if graphs-txid.edn's content isn't [user-uuid graph-uuid txid], clear it
       (if (not= 3 (count @graphs-txid))
-        (clear-graphs-txid! repo)
+        (do (clear-graphs-txid! repo)
+            (state/set-file-sync-state nil))
         (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
           (if-not (<! (check-remote-graph-exists graph-uuid))
             (clear-graphs-txid! repo)
             (do
               ;; set-env
               (set-env rsapi config/FILE-SYNC-PROD?)
-
+              (state/set-file-sync-state @*sync-state)
+              (state/set-file-sync-manager sm)
+              ;; wait seconds to receive all file change events,
+              ;; and then drop all of them.
+              ;; WHY: when opening a graph(or switching to another graph),
+              ;;      file-watcher will send a lot of file-change-events,
+              ;;      actually, each file corresponds to a file-change-event,
+              ;;      we need to ignore all of them.
+              (<! (timeout 5000))
               (drain-chan local-changes-chan)
               (poll! stop-sync-chan)
               (poll! remote->local-sync-chan)
+
               ;; update global state when *sync-state changes
               (add-watch *sync-state ::update-global-state
                          (fn [_ _ _ n]
                            (state/set-file-sync-state n)))
               (.start sm)
 
-              (state/set-file-sync-manager sm)
+
               (offer! remote->local-sync-chan true)
               (offer! full-sync-chan true)
 

+ 1 - 1
src/main/frontend/fs/watcher_handler.cljs

@@ -4,7 +4,7 @@
             [frontend.db :as db]
             [frontend.db.model :as model]
             [frontend.handler.editor :as editor]
-            [frontend.handler.extract :as extract]
+            [logseq.graph-parser.extract :as extract]
             [frontend.handler.file :as file-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]

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

@@ -93,7 +93,7 @@
            (and (not (seq (db/get-files config/local-repo)))
                 ;; Not native local directory
                 (not (some config/local-db? (map :url repos)))
-                (not (mobile-util/is-native-platform?)))
+                (not (mobile-util/native-platform?)))
            ;; will execute `(state/set-db-restoring! false)` inside
            (repo-handler/setup-local-repo-if-not-exists!)
 
@@ -193,7 +193,7 @@
     (p/let [repos (get-repos)]
       (state/set-repos! repos)
       (restore-and-setup! repos db-schema)
-      (when (mobile-util/is-native-platform?)
+      (when (mobile-util/native-platform?)
         (p/do! (mobile-util/hide-splash))))
 
     (reset! db/*sync-search-indice-f search/sync-search-indice!)

+ 3 - 3
src/main/frontend/handler/block.cljs

@@ -12,8 +12,8 @@
    [frontend.modules.outliner.transaction :as outliner-tx]
    [frontend.state :as state]
    [frontend.util :as util]
-   [goog.dom :as gdom]))
-
+   [goog.dom :as gdom]
+   [logseq.graph-parser.block :as gp-block]))
 
 ;;  Fns
 
@@ -89,7 +89,7 @@
   [block typ]
   (walk-block block
               (fn [x]
-                (and (block/timestamp-block? x)
+                (and (gp-block/timestamp-block? x)
                      (= typ (first (second x)))))
               #(second (second %))))
 

+ 81 - 90
src/main/frontend/handler/editor.cljs

@@ -1,13 +1,15 @@
 (ns frontend.handler.editor
   (:require ["/frontend/utils" :as utils]
+            ["path" :as path]
             [cljs.core.match :refer [match]]
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as w]
             [dommy.core :as dom]
             [frontend.commands :as commands
-             :refer [*angle-bracket-caret-pos *show-block-commands
-                     *show-commands *slash-caret-pos]]
+             :refer [*angle-bracket-caret-pos
+                     *show-block-commands *show-commands
+                     *slash-caret-pos]]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -25,35 +27,35 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
-            [frontend.image :as image]
             [frontend.idb :as idb]
+            [frontend.image :as image]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.core :as outliner-core]
-            [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.transaction :as outliner-tx]
+            [frontend.modules.outliner.tree :as tree]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.template :as template]
-            [frontend.text :as text]
-            [frontend.utf8 :as utf8]
+            [logseq.graph-parser.text :as text]
+            [logseq.graph-parser.utf8 :as utf8]
             [frontend.util :as util :refer [profile]]
             [frontend.util.clock :as clock]
             [frontend.util.cursor :as cursor]
             [frontend.util.drawer :as drawer]
+            [frontend.util.keycode :as keycode]
+            [frontend.util.list :as list]
             [frontend.util.marker :as marker]
-            [frontend.util.property :as property]
             [frontend.util.priority :as priority]
+            [frontend.util.property :as property]
             [frontend.util.thingatpt :as thingatpt]
-            [frontend.util.list :as list]
             [goog.dom :as gdom]
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [medley.core :as medley]
             [promesa.core :as p]
-            [frontend.util.keycode :as keycode]
             [logseq.graph-parser.util :as gp-util]
-            ["path" :as path]))
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.block :as gp-block]))
 
 ;; FIXME: should support multiple images concurrently uploading
 
@@ -254,10 +256,9 @@
 
 (defn- another-block-with-same-id-exists?
   [current-id block-id]
-  (and (string? block-id)
-       (gp-util/uuid-string? block-id)
-       (not= current-id (cljs.core/uuid block-id))
-       (db/entity [:block/uuid (cljs.core/uuid block-id)])))
+  (when-let [id (and (string? block-id) (parse-uuid block-id))]
+    (and (not= current-id id)
+         (db/entity [:block/uuid id]))))
 
 (defn- attach-page-properties-if-exists!
   [block]
@@ -336,7 +337,7 @@
   (if (and (state/enable-timetracking?)
            (not= (:block/content block) value))
     (let [format (:block/format block)
-          new-marker (last (gp-util/safe-re-find (marker/marker-pattern format) (or value "")))
+          new-marker (last (util/safe-re-find (marker/marker-pattern format) (or value "")))
           new-value (with-marker-time value block format
                       new-marker
                       (:block/marker block))]
@@ -356,7 +357,7 @@
         content (drawer/with-logbook block content)
         content (with-timetracking block content)
         first-block? (= left page)
-        ast (mldoc/->edn (string/trim content) (mldoc/default-config format))
+        ast (mldoc/->edn (string/trim content) (gp-mldoc/default-config format))
         first-elem-type (first (ffirst ast))
         first-elem-meta (second (ffirst ast))
         properties? (contains? #{"Property_Drawer" "Properties"} first-elem-type)
@@ -471,7 +472,7 @@
                    (not has-children?))]
     (outliner-tx/transact!
       {:outliner-op :insert-blocks}
-      (save-current-block!)
+      (save-current-block! {:current-block current-block})
       (outliner-core/insert-blocks! [new-block] current-block {:sibling? sibling?
                                                                :keep-uuid? keep-uuid?
                                                                :replace-empty-target? replace-empty-target?}))))
@@ -479,14 +480,9 @@
 (defn- block-self-alone-when-insert?
   [config uuid]
   (let [current-page (state/get-current-page)
-        block-id (or
-                  (and (:id config)
-                       (gp-util/uuid-string? (:id config))
-                       (:id config))
-                  (and current-page
-                       (gp-util/uuid-string? current-page)
-                       current-page))]
-    (= uuid (and block-id (medley/uuid block-id)))))
+        block-id (or (some-> (:id config) parse-uuid)
+                     (some-> current-page parse-uuid))]
+    (= uuid block-id)))
 
 (defn insert-new-block-before-block-aux!
   [config block _value {:keys [ok-handler]}]
@@ -668,7 +664,9 @@
 (defn properties-block
   [properties format page]
   (let [content (property/insert-properties format "" properties)
-        refs (block/get-page-refs-from-properties properties)]
+        refs (gp-block/get-page-refs-from-properties properties
+                                                     (db/get-db (state/get-current-repo))
+                                                     (state/get-date-formatter))]
     {:block/pre-block? true
      :block/uuid (db/new-block-id)
      :block/properties properties
@@ -1141,7 +1139,7 @@
   []
   (when-let [page (get-nearest-page)]
     (let [page-name (string/lower-case page)
-          block? (gp-util/uuid-string? page-name)]
+          block? (util/uuid-string? page-name)]
       (when-let [page (db/get-page page-name)]
         (if block?
           (state/sidebar-add-block!
@@ -1169,10 +1167,7 @@
   []
   (if (state/editing?)
     (let [page (state/get-current-page)
-          block-id (and
-                    (string? page)
-                    (gp-util/uuid-string? page)
-                    (medley/uuid page))]
+          block-id (and (string? page) (parse-uuid page))]
       (when block-id
         (let [block-parent (db/get-block-parent block-id)]
           (if-let [id (and
@@ -1272,7 +1267,7 @@
   "skip-properties? if set true, when editing block is likely be properties, skip saving"
   ([]
    (save-current-block! {}))
-  ([{:keys [force? skip-properties?] :as opts}]
+  ([{:keys [force? skip-properties? current-block] :as opts}]
    ;; non English input method
    (when-not (state/editor-in-composition?)
      (when (state/get-current-repo)
@@ -1293,7 +1288,8 @@
                  db-content (:block/content db-block)
                  db-content-without-heading (and db-content
                                                  (gp-util/safe-subs db-content (:block/level db-block)))
-                 value (and elem (gobj/get elem "value"))]
+                 value (or (:block/content current-block)
+                           (and elem (gobj/get elem "value")))]
              (cond
                force?
                (save-block-aux! db-block value opts)
@@ -1312,7 +1308,7 @@
 
 (defn- clean-content!
   [format content]
-  (->> (text/remove-level-spaces content format)
+  (->> (text/remove-level-spaces content format (config/get-block-pattern format))
        (drawer/remove-logbook)
        (property/remove-properties format)
        string/trim))
@@ -1348,7 +1344,7 @@
 
 (defn get-asset-file-link
   [format url file-name image?]
-  (let [pdf? (and url (string/ends-with? url ".pdf"))]
+  (let [pdf? (and url (string/ends-with? (string/lower-case url) ".pdf"))]
     (case (keyword format)
       :markdown (util/format (str (when (or image? pdf?) "!") "[%s](%s)") file-name url)
       :org (if image?
@@ -1414,7 +1410,7 @@
       (util/electron?)
       (str "assets://" repo-dir path)
 
-      (mobile-util/is-native-platform?)
+      (mobile-util/native-platform?)
       (mobile-util/convert-file-src (str repo-dir path))
 
       :else
@@ -1680,7 +1676,8 @@
             (move-nodes blocks))
           (when-let [input-id (state/get-edit-input-id)]
             (when-let [input (gdom/getElement input-id)]
-              (.focus input))))
+              (.focus input)
+              (js/setTimeout #(util/scroll-editor-cursor input) 100))))
         (let [ids (state/get-selection-block-ids)]
           (when (seq ids)
             (let [lookup-refs (map (fn [id] [:block/uuid id]) ids)
@@ -1701,7 +1698,8 @@
   (let [blocks (get-selected-ordered-blocks)]
     (when (seq blocks)
       (outliner-tx/transact!
-        {:outliner-op :move-blocks}
+        {:outliner-op :move-blocks
+         :real-outliner-op :indent-outdent}
         (outliner-core/indent-outdent-blocks! blocks (= direction :right))))))
 
 (defn- get-link [format link label]
@@ -1940,7 +1938,7 @@
                     props (into [] (:properties block))
                     content* (str (if (= :markdown format) "- " "* ")
                                   (property/insert-properties format content props))
-                    ast (mldoc/->edn content* (mldoc/default-config format))
+                    ast (mldoc/->edn content* (gp-mldoc/default-config format))
                     blocks (block/extract-blocks ast content* true format)
                     fst-block (first blocks)]
                 (assert fst-block "fst-block shouldn't be nil")
@@ -1953,7 +1951,7 @@
   (let [blocks (block-tree->blocks tree-vec format)
         target-block (db/pull target-block-id)
         page-id (:db/id (:block/page target-block))
-        blocks (block/with-parent-and-left page-id blocks)]
+        blocks (gp-block/with-parent-and-left page-id blocks)]
     (paste-blocks
      blocks
      {:target-block target-block
@@ -2026,15 +2024,16 @@
   (when-not (parent-is-page? node)
     (let [parent-node (tree/-get-parent node)]
       (outliner-tx/transact!
-        {:outliner-op :move-blocks}
+        {:outliner-op :move-blocks
+         :real-outliner-op :indent-outdent}
         (save-current-block!)
         (outliner-core/move-blocks! [(:data node)] (:data parent-node) true)))))
 
 (defn- last-top-level-child?
   [{:keys [id]} current-node]
   (when id
-    (when-let [entity (if (gp-util/uuid-string? (str id))
-                        (db/entity [:block/uuid (uuid id)])
+    (when-let [entity (if-let [id' (parse-uuid (str id))]
+                        (db/entity [:block/uuid id'])
                         (db/entity [:block/name (util/page-name-sanity-lc id)]))]
       (= (:block/uuid entity) (tree/-get-parent-id current-node)))))
 
@@ -2583,7 +2582,8 @@
     (when block
       (state/set-editor-last-pos! pos)
       (outliner-tx/transact!
-        {:outliner-op :move-blocks}
+        {:outliner-op :move-blocks
+         :real-outliner-op :indent-outdent}
         (save-current-block!)
         (outliner-core/indent-outdent-blocks! [block] indent?)))
     (state/set-editor-op! :nil)))
@@ -2630,7 +2630,7 @@
         ;; FIXME: On mobile, a backspace click to call keydown-backspace-handler
         ;; does not work sometimes in an empty block, hence the empty block
         ;; can't be deleted. Need to figure out why and find a better solution.
-        (and (mobile-util/is-native-platform?)
+        (and (mobile-util/native-platform?)
              (= key "Backspace")
              (= value ""))
         (do
@@ -2813,24 +2813,9 @@
   [id]
   (fn [_e]
     (let [input (gdom/getElement id)]
+      (util/scroll-editor-cursor input)
       (close-autocomplete-if-outside input))))
 
-(defonce mobile-toolbar-height 40)
-(defn editor-on-height-change!
-  [id]
-  (fn [box-height ^js row-height]
-    (let [row-height (:rowHeight (js->clj row-height :keywordize-keys true))
-          input (gdom/getElement id)
-          caret (cursor/get-caret-pos input)
-          cursor-bottom (if caret (+ row-height (:top caret)) box-height)
-          box-top (gobj/get (.getBoundingClientRect input) "top")
-          cursor-y (+ cursor-bottom box-top)
-          vw-height (.-height js/window.visualViewport)]
-      (when (<  vw-height (+ cursor-y mobile-toolbar-height))
-        (let [main-node (gdom/getElement "main-content-container")
-              scroll-top (.-scrollTop main-node)]
-          (set! (.-scrollTop main-node) (+ scroll-top row-height)))))))
-
 (defn editor-on-change!
   [block id search-timeout]
   (fn [e]
@@ -2842,15 +2827,17 @@
                 (js/setTimeout
                  #(edit-box-on-change! e block id)
                  timeout)))
-      (edit-box-on-change! e block id))))
+      (let [input (gdom/getElement id)]
+        (edit-box-on-change! e block id)
+        (util/scroll-editor-cursor input)))))
 
 (defn- paste-text-parseable
   [format text]
   (when-let [editing-block (state/get-edit-block)]
     (let [page-id (:db/id (:block/page editing-block))
           blocks (block/extract-blocks
-                  (mldoc/->edn text (mldoc/default-config format)) text true format)
-          blocks' (block/with-parent-and-left page-id blocks)]
+                  (mldoc/->edn text (gp-mldoc/default-config format)) text true format)
+          blocks' (gp-block/with-parent-and-left page-id blocks)]
       (paste-blocks blocks' {}))))
 
 (defn- paste-segmented-text
@@ -2860,7 +2847,7 @@
         (string/join "\n"
                      (mapv (fn [p] (->> (string/trim p)
                                         ((fn [p]
-                                           (if (gp-util/safe-re-find (if (= format :org)
+                                           (if (util/safe-re-find (if (= format :org)
                                                                     #"\s*\*+\s+"
                                                                     #"\s*-\s+") p)
                                              p
@@ -2878,6 +2865,18 @@
         (recur (remove (set (map :block/uuid result)) (rest ids)) result))
       result)))
 
+(defn wrap-macro-url
+  [url]
+  (cond
+    (boolean (text/get-matched-video url))
+    (util/format "{{video %s}}" url)
+
+    (string/includes? url "twitter.com")
+    (util/format "{{twitter %s}}" url)
+
+    :else
+    (notification/show! (util/format "No macro is available for %s" url) :warning)))
+
 (defn- paste-copied-blocks-or-text
   [text e]
   (let [copied-blocks (state/get-copied-blocks)
@@ -2902,21 +2901,10 @@
           (state/set-copied-full-blocks! blocks)
           (paste-blocks blocks {})))
 
-      (and (util/url? text)
+      (and (gp-util/url? text)
            (not (string/blank? (util/get-selected-text))))
       (html-link-format! text)
-
-      (and (util/url? text)
-           (or (string/includes? text "youtube.com")
-               (string/includes? text "youtu.be"))
-           (mobile-util/is-native-platform?))
-      (commands/simple-insert! (state/get-edit-input-id) (util/format "{{youtube %s}}" text) nil)
-
-      (and (util/url? text)
-           (string/includes? text "twitter.com")
-           (mobile-util/is-native-platform?))
-      (commands/simple-insert! (state/get-edit-input-id) (util/format "{{twitter %s}}" text) nil)
-
+      
       (and (text/block-ref? text)
            (wrapped-by? input "((" "))"))
       (commands/simple-insert! (state/get-edit-input-id) (text/get-block-ref text) nil)
@@ -2925,9 +2913,9 @@
       ;; from external
       (let [format (or (db/get-page-format (state/get-current-page)) :markdown)]
         (match [format
-                (nil? (gp-util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
-                (nil? (gp-util/safe-re-find #"(?m)^\s*\*+\s+" text))
-                (nil? (gp-util/safe-re-find #"(?:\r?\n){2,}" text))]
+                (nil? (util/safe-re-find #"(?m)^\s*(?:[-+*]|#+)\s+" text))
+                (nil? (util/safe-re-find #"(?m)^\s*\*+\s+" text))
+                (nil? (util/safe-re-find #"(?:\r?\n){2,}" text))]
           [:markdown false _ _]
           (paste-text-parseable format text)
 
@@ -2953,7 +2941,10 @@
   (utils/getClipText
    (fn [clipboard-data]
      (when-let [_ (state/get-input)]
-       (state/append-current-edit-content! clipboard-data)))
+       (let [data (if (gp-util/url? clipboard-data)
+                        (wrap-macro-url clipboard-data)
+                        clipboard-data)]
+             (state/append-current-edit-content! data))))
    (fn [error]
      (js/console.error error))))
 
@@ -2964,7 +2955,8 @@
     (let [text (.getData (gobj/get e "clipboardData") "text")
           input (state/get-input)]
       (if-not (string/blank? text)
-        (if (thingatpt/org-admonition&src-at-point input)
+        (if (or (thingatpt/markdown-src-at-point input)
+                (thingatpt/org-admonition&src-at-point input))
           (when-not (mobile-util/native-ios?)
             (util/stop e)
             (paste-text-in-one-block-at-point))
@@ -3100,7 +3092,7 @@
   (when-let [block-id (some-> (state/get-selection-blocks)
                               first
                               (dom/attr "blockid")
-                              medley/uuid)]
+                              uuid)]
     (util/stop e)
     (let [block    {:block/uuid block-id}
           block-id (-> (state/get-selection-blocks)
@@ -3169,7 +3161,7 @@
   [format content semantic?]
   (and (string/includes? content "\n")
        (if semantic?
-         (let [ast (mldoc/->edn content (mldoc/default-config format))
+         (let [ast (mldoc/->edn content (gp-mldoc/default-config format))
                first-elem-type (first (ffirst ast))]
            (mldoc/block-with-title? first-elem-type))
          true)))
@@ -3212,8 +3204,7 @@
     :or {collapse? false expanded? false incremental? true root-block nil}}]
   (when-let [page (or (state/get-current-page)
                       (date/today))]
-    (let [block? (gp-util/uuid-string? page)
-          block-id (or root-block (and block? (uuid page)))
+    (let [block-id (or root-block (parse-uuid page))
           blocks (if block-id
                    (db/get-block-and-children (state/get-current-repo) block-id)
                    (db/get-page-blocks-no-cache page))
@@ -3310,7 +3301,7 @@
        (->> (get-selected-blocks)
             (map (fn [dom]
                    (-> (dom/attr dom "blockid")
-                       medley/uuid
+                       uuid
                        expand-block!)))
             doall)
        (and clear-selection? (clear-selection!)))
@@ -3343,7 +3334,7 @@
        (->> (get-selected-blocks)
             (map (fn [dom]
                    (-> (dom/attr dom "blockid")
-                       medley/uuid
+                       uuid
                        collapse-block!)))
             doall)
        (and clear-selection? (clear-selection!)))

+ 1 - 4
src/main/frontend/handler/editor/lifecycle.cljs

@@ -3,7 +3,6 @@
             [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.mobile.util :as mobile-util]
             [goog.dom :as gdom]))
 
 (defn did-mount!
@@ -21,9 +20,7 @@
 
     (when-let [element (gdom/getElement id)]
       (.focus element)
-      (when (or (mobile-util/is-native-platform?)
-                (util/mobile?))
-        (util/make-el-cursor-position-into-center-viewport element))))
+      (js/setTimeout #(util/scroll-editor-cursor element) 50)))
   state)
 
 (defn did-remount!

+ 25 - 13
src/main/frontend/handler/events.cljs

@@ -59,8 +59,8 @@
 (defn- file-sync-stop-when-switch-graph []
   (p/do! (persist-var/load-vars)
          (sync/sync-stop)
-         ;; trigger rerender file-sync-header
-         (state/set-file-sync-state nil)))
+         (sync/sync-start)
+))
 
 (defn- graph-switch [graph]
   (state/set-current-repo! graph)
@@ -98,7 +98,6 @@
      (graph-switch graph))))
 
 (defmethod handle :graph/switch [[_ graph]]
-  (file-sync-stop-when-switch-graph)
   (if (outliner-file/writes-finished?)
     (graph-switch-on-persisted graph)
     (notification/show!
@@ -138,7 +137,7 @@
   [repo]
   (when
    (and (not (util/electron?))
-        (not (mobile-util/is-native-platform?)))
+        (not (mobile-util/native-platform?)))
     (fn [close-fn]
       [:div
        [:p
@@ -289,15 +288,28 @@
     (reset! st/*inited? true)
     (st/consume-pending-shortcuts!)))
 
-
-(defmethod handle :mobile/keyboard-will-show [[_]]
-  (when (and (state/get-left-sidebar-open?)
-             (state/editing?))
-    (state/set-left-sidebar-open! false)))
-
-(defmethod handle :mobile/keyboard-did-show [[_]]
-  (when-let [input (state/get-input)]
-    (util/make-el-cursor-position-into-center-viewport input)))
+(defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
+  (let [main-node (util/app-scroll-container-node)]
+    (state/set-state! :mobile/show-tabbar? false)
+    (state/set-state! :mobile/show-toolbar? true)
+    (when (mobile-util/native-ios?)
+      (reset! util/keyboard-height keyboard-height)
+      (set! (.. main-node -style -marginBottom) (str keyboard-height "px"))
+      (when-let [card-preview-el (js/document.querySelector ".cards-review")]
+        (set! (.. card-preview-el -style -marginBottom) (str keyboard-height "px")))
+      (js/setTimeout (fn []
+                       (let [toolbar (.querySelector main-node "#mobile-editor-toolbar")]
+                         (set! (.. toolbar -style -bottom) (str keyboard-height "px"))))
+                     100))))
+
+(defmethod handle :mobile/keyboard-will-hide [[_]]
+  (let [main-node (util/app-scroll-container-node)]
+    (state/set-state! :mobile/show-toolbar? false)
+    (state/set-state! :mobile/show-tabbar? true)
+    (when (mobile-util/native-ios?)
+      (when-let [card-preview-el (js/document.querySelector ".cards-review")]
+        (set! (.. card-preview-el -style -marginBottom) "0px"))
+      (set! (.. main-node -style -marginBottom) "0px"))))
 
 (defmethod handle :plugin/consume-updates [[_ id pending? updated?]]
   (let [downloading? (:plugin/updates-downloading? @state/state)]

+ 5 - 3
src/main/frontend/handler/export.cljs

@@ -16,6 +16,8 @@
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.format.mldoc :as mldoc]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.util :as gp-util]
             [goog.dom :as gdom]
             [promesa.core :as p])
   (:import [goog.string StringBuffer]))
@@ -214,7 +216,7 @@
   (let [block (db/entity [:block/uuid (uuid block-uuid)])
         block-content (get-blocks-contents repo (:block/uuid block))
         format (:block/format block)
-        ast (mldoc/->edn block-content (mldoc/default-config format))
+        ast (mldoc/->edn block-content (gp-mldoc/default-config format))
         embed-pages-new  (get-embed-pages-from-ast ast)
         embed-blocks-new  (get-embed-blocks-from-ast ast)
         block-refs-new (get-block-refs-from-ast ast)
@@ -258,7 +260,7 @@
   (let [page-name* (util/page-name-sanity-lc page-name)
         page-content (get-page-content repo page-name*)
         format (:block/format (db/entity [:block/name page-name*]))
-        ast (mldoc/->edn page-content (mldoc/default-config format))
+        ast (mldoc/->edn page-content (gp-mldoc/default-config format))
         embed-pages-new (get-embed-pages-from-ast ast)
         embed-blocks-new (get-embed-blocks-from-ast ast)
         block-refs-new (get-block-refs-from-ast ast)
@@ -378,7 +380,7 @@
                                               [?e2 :block/file ?e]
                                               [?e2 :block/name ?n]
                                               [?e2 :block/original-name ?n2]] db path)
-                                :format (f/get-format path)})))))
+                                :format (gp-util/get-format path)})))))
 
 
 (defn export-repo-as-markdown!

+ 4 - 2
src/main/frontend/handler/external.cljs

@@ -9,6 +9,8 @@
             [frontend.db :as db]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.block :as block]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.date-time-util :as date-time-util]
             [frontend.handler.page :as page]
             [frontend.handler.editor :as editor]
             [frontend.util :as util]))
@@ -26,7 +28,7 @@
                                       (when journal?
                                         (date/journal-title->default title))
                                       (string/replace title "/" "-"))
-                               title (-> (util/page-name-sanity title)
+                               title (-> (gp-util/page-name-sanity title)
                                          (string/replace "\n" " "))
                                path (str (if journal?
                                            (config/get-journals-directory)
@@ -49,7 +51,7 @@
                              (map
                               (fn [title]
                                 (let [day (date/journal-title->int title)
-                                      page-name (util/page-name-sanity-lc (date/int->journal-title day))]
+                                      page-name (util/page-name-sanity-lc (date-time-util/int->journal-title day (state/get-date-formatter)))]
                                   {:block/name page-name
                                    :block/journal? true
                                    :block/journal-day day}))

+ 16 - 7
src/main/frontend/handler/file.cljs

@@ -8,15 +8,15 @@
             [clojure.core.async :as async]
             [frontend.config :as config]
             [frontend.db :as db]
-            [frontend.format :as format]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
             [frontend.handler.common :as common-handler]
-            [frontend.handler.extract :as extract-handler]
+            [logseq.graph-parser.extract :as extract]
             [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.util :as util]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.config :as gp-config]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
             [frontend.mobile.util :as mobile]
@@ -43,7 +43,7 @@
   [files formats]
   (filter
    (fn [file]
-     (let [format (format/get-format file)]
+     (let [format (gp-util/get-format file)]
        (contains? formats format)))
    files))
 
@@ -119,10 +119,19 @@
          file (gp-util/path-normalize file)
          new? (nil? (db/entity [:file/path file]))]
      (db/set-file-content! repo-url file content)
-     (let [format (format/get-format file)
+     (let [format (gp-util/get-format file)
            file-content [{:file/path file}]
-           tx (if (contains? config/mldoc-support-formats format)
-                (let [[pages blocks] (extract-handler/extract-blocks-pages repo-url file content)
+           tx (if (contains? gp-config/mldoc-support-formats format)
+                (let [[pages blocks]
+                      (extract/extract-blocks-pages
+                       file
+                       content
+                       {:user-config (state/get-config)
+                        :date-formatter (state/get-date-formatter)
+                        :page-name-order (state/page-name-order)
+                        :block-pattern (config/get-block-pattern format)
+                        :supported-formats (config/supported-formats)
+                        :db (db/get-db (state/get-current-repo))})
                       first-page (first pages)
                       delete-blocks (->
                                      (concat
@@ -144,7 +153,7 @@
                                           (seq))
                       ;; To prevent "unique constraint" on datascript
                       block-ids (set/union (set block-ids) (set block-refs-ids))
-                      pages (extract-handler/with-ref-pages pages blocks)
+                      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))

+ 97 - 14
src/main/frontend/handler/file_sync.cljs

@@ -1,15 +1,19 @@
 (ns frontend.handler.file-sync
   (:require ["path" :as path]
             [cljs-time.coerce :as tc]
+            [cljs-time.format :as tf]
             [cljs.core.async :as async :refer [go <!]]
+            [cljs.core.async.interop :refer [p->c]]
             [clojure.string :as string]
+            [clojure.set :as set]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.fs.sync :as sync]
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.handler.user :as user]))
+            [frontend.handler.user :as user]
+            [frontend.fs :as fs]))
 
 (def hiding-login&file-sync (not config/dev?))
 (def refresh-file-sync-component (atom false))
@@ -51,9 +55,41 @@
   []
   (go (:Graphs (<! (sync/list-remote-graphs sync/remoteapi)))))
 
+(defn download-all-files
+  [repo graph-uuid user-uuid base-path]
+  (go
+    (state/reset-file-sync-download-init-state!)
+    (state/set-file-sync-download-init-state! {:total js/NaN :finished 0 :downloading? true})
+    (let [remote-all-files-meta (<! (sync/get-remote-all-files-meta sync/remoteapi graph-uuid))
+          local-all-files-meta (<! (sync/get-local-all-files-meta sync/rsapi graph-uuid base-path))
+          diff-remote-files (set/difference remote-all-files-meta local-all-files-meta)
+          latest-txid (:TXId (<! (sync/get-remote-graph sync/remoteapi nil graph-uuid)))
+          partitioned-filetxns
+          (sequence (sync/filepaths->partitioned-filetxns 10 graph-uuid user-uuid)
+                    (map sync/relative-path diff-remote-files))]
+      (state/set-file-sync-download-init-state! {:total (count diff-remote-files) :finished 0})
+      (let [r (<! (sync/apply-filetxns-partitions
+                   nil user-uuid graph-uuid base-path partitioned-filetxns repo nil (atom false)
+                   (fn [filetxns]
+                     (state/set-file-sync-download-init-state!
+                      {:downloading-files (mapv sync/relative-path filetxns)}))
+                   (fn [filetxns]
+                     (state/set-file-sync-download-init-state!
+                      {:finished (+ (count filetxns)
+                                    (or (:finished (state/get-file-sync-download-init-state)) 0))}))))]
+        (if (instance? ExceptionInfo r)
+          ;; TODO: add re-download button
+          (notification/show! (str "Download graph failed: " (ex-cause r)) :warning)
+          (do (state/reset-file-sync-download-init-state!)
+              (sync/update-graphs-txid! latest-txid graph-uuid user-uuid repo)))))))
+
 (defn switch-graph [graph-uuid]
-  (sync/update-graphs-txid! 0 graph-uuid (user/user-uuid) (state/get-current-repo))
-  (swap! refresh-file-sync-component not))
+  (let [repo (state/get-current-repo)
+        base-path (config/get-repo-dir repo)
+        user-uuid (user/user-uuid)]
+    (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
+    (download-all-files repo graph-uuid user-uuid base-path)
+    (swap! refresh-file-sync-component not)))
 
 (defn- download-version-file [graph-uuid file-uuid version-uuid]
 
@@ -65,31 +101,78 @@
         (notification/show! (ex-cause r) :error)
         (notification/show! [:div
                              [:div "Downloaded version file at: "]
-                             [:div key]] :success false)))))
+                             [:div key]] :success false))
+      (when-not (instance? ExceptionInfo r)
+        key))))
+
+(defn- list-file-local-versions
+  [page]
+  (go
+    (when-let [path (-> page :block/file :file/path)]
+      (let [base-path           (config/get-repo-dir (state/get-current-repo))
+            rel-path            (string/replace-first path base-path "")
+            version-files-dir   (->> (path/join "version-files/local" rel-path)
+                                     path/parse
+                                     (#(js->clj % :keywordize-keys true))
+                                     ((juxt :dir :name))
+                                     (apply path/join base-path))
+            version-file-paths* (<! (p->c (fs/readdir version-files-dir)))]
+        (when-not (instance? ExceptionInfo version-file-paths*)
+          (let [version-file-paths
+                (filterv
+                 ;; filter dir
+                 (fn [dir-or-file]
+                   (-> (path/parse dir-or-file)
+                       (js->clj :keywordize-keys true)
+                       :ext
+                       seq))
+                 (js->clj (<! (p->c (fs/readdir version-files-dir)))))]
+            (mapv
+             (fn [path]
+               (let [create-time
+                     (-> (path/parse path)
+                         (js->clj :keywordize-keys true)
+                         :name
+                         (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
+                 {:create-time create-time :path path :relative-path (string/replace-first path base-path "")}))
+             version-file-paths)))))))
 
 (defn list-file-versions [graph-uuid page]
   (let [file-id (:db/id (:block/file page))]
     (when-let [path (:file/path (db/entity file-id))]
       (let [base-path (config/get-repo-dir (state/get-current-repo))
-            path* (string/replace-first path base-path "")]
+            path*     (string/replace-first path base-path "")]
         (go
-          (let [version-list (:VersionList
-                              (<! (sync/get-remote-file-versions sync/remoteapi graph-uuid path*)))]
+          (let [version-list       (:VersionList
+                                    (<! (sync/get-remote-file-versions sync/remoteapi graph-uuid path*)))
+                local-version-list (<! (list-file-local-versions page))
+                all-version-list   (->> (concat version-list local-version-list)
+                                        (sort-by #(or (tc/from-string (:CreateTime %))
+                                                      (:create-time %))
+                                                 >))]
             (notification/show! [:div
                                  [:div.font-bold "File history - " path*]
                                  [:hr.my-2]
-                                 (for [version version-list]
-                                   (let [version-uuid (:VersionUUID version)]
+                                 (for [version all-version-list]
+                                   (let [version-uuid (or (:VersionUUID version) (:relative-path version))
+                                         local?       (some? (:relative-path version))]
                                      [:div.my-4 {:key version-uuid}
                                       [:div
                                        [:a.text-xs.inline
-                                        {:on-click #(download-version-file graph-uuid
-                                                                           (:FileUUID version)
-                                                                           (:VersionUUID version))}
+                                        {:on-click #(if local?
+                                                      (js/window.apis.openPath (:path version))
+                                                      (go
+                                                        (let [relative-path
+                                                              (<! (download-version-file graph-uuid
+                                                                                         (:FileUUID version)
+                                                                                         (:VersionUUID version)))]
+                                                          (js/window.apis.openPath (path/join base-path relative-path)))))}
                                         version-uuid]
-                                       [:div.opacity-70 (str "Size: " (:Size version))]]
+                                       (when-not local?
+                                         [:div.opacity-70 (str "Size: " (:Size version))])]
                                       [:div.opacity-50
-                                       (util/time-ago (tc/from-string (:CreateTime version)))]]))]
+                                       (util/time-ago (or (tc/from-string (:CreateTime version))
+                                                          (:create-time version)))]]))]
                                 :success false)))))))
 
 (defn get-current-graph-uuid [] (second @sync/graphs-txid))

+ 2 - 3
src/main/frontend/handler/graph.cljs

@@ -4,8 +4,7 @@
             [frontend.db :as db]
             [frontend.db.default :as default-db]
             [frontend.state :as state]
-            [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]))
+            [frontend.util :as util]))
 
 (defn- build-links
   [links]
@@ -46,7 +45,7 @@
                   ;; slow
 (defn- uuid-or-asset?
   [id]
-  (or (gp-util/uuid-string? id)
+  (or (util/uuid-string? id)
       (string/starts-with? id "../assets/")
       (= id "..")
       (string/starts-with? id "assets/")

+ 15 - 12
src/main/frontend/handler/page.cljs

@@ -11,7 +11,6 @@
             [frontend.db.model :as model]
             [frontend.db.utils :as db-utils]
             [frontend.db.conn :as conn]
-            [frontend.format.block :as block]
             [frontend.fs :as fs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
@@ -35,6 +34,8 @@
             [frontend.mobile.util :as mobile-util]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.block :as gp-block]
+            [frontend.format.block :as block]
             [goog.functions :refer [debounce]]))
 
 (defn- get-directory
@@ -47,7 +48,7 @@
   [journal? title]
   (when-let [s (if journal?
                  (date/journal-title->default title)
-                 (util/page-name-sanity (string/lower-case title)))]
+                 (gp-util/page-name-sanity (string/lower-case title)))]
     ;; Win10 file path has a length limit of 260 chars
     (gp-util/safe-subs s 0 200)))
 
@@ -72,7 +73,9 @@
    (let [p (common-handler/get-page-default-properties title)
          ps (merge p properties)
          content (page-property/insert-properties format "" ps)
-         refs (block/get-page-refs-from-properties properties)]
+         refs (gp-block/get-page-refs-from-properties properties
+                                                      (db/get-db (state/get-current-repo))
+                                                      (state/get-date-formatter))]
      {:block/uuid (db/new-block-id)
       :block/properties ps
       :block/properties-order (keys ps)
@@ -118,12 +121,12 @@
                   properties          nil
                   split-namespace?    true}}]
    (let [title (string/trim title)
-         title (util/remove-boundary-slashes title)
+         title (gp-util/remove-boundary-slashes title)
          page-name (util/page-name-sanity-lc title)
          repo (state/get-current-repo)]
      (when (db/page-empty? repo page-name)
        (let [pages    (if split-namespace?
-                        (util/split-namespace-pages title)
+                        (gp-util/split-namespace-pages title)
                         [title])
              format   (or format (state/get-preferred-format))
              pages    (map (fn [page]
@@ -161,7 +164,7 @@
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (->
        (p/let [_ (and (config/local-db? repo)
-                      (mobile-util/is-native-platform?)
+                      (mobile-util/native-platform?)
                       (fs/delete-file! repo file-path file-path {}))
                _ (fs/unlink! repo (config/get-repo-path repo file-path) nil)])
        (p/catch (fn [err]
@@ -170,7 +173,7 @@
 (defn- compute-new-file-path
   [old-path new-name]
   (let [result (string/split old-path "/")
-        file-name (util/page-name-sanity new-name true)
+        file-name (gp-util/page-name-sanity new-name true)
         ext (last (string/split (last result) "."))
         new-file (str file-name "." ext)
         parts (concat (butlast result) [new-file])]
@@ -398,7 +401,7 @@
 
         ;; If page name changed after sanitization
         (when (or (util/create-title-property? new-page-name)
-                  (not= (util/page-name-sanity new-name false) new-name))
+                  (not= (gp-util/page-name-sanity new-name false) new-name))
           (page-property/add-property! new-page-name :title new-name))
 
         (when (and file (not journal?))
@@ -591,7 +594,7 @@
                      page)
         (let [journal? (date/valid-journal-title? page)
               ref-file-path (str
-                             (if (or (util/electron?) (mobile-util/is-native-platform?))
+                             (if (or (util/electron?) (mobile-util/native-platform?))
                                (-> (config/get-repo-dir (state/get-current-repo))
                                    js/decodeURI
                                    (string/replace #"/+$" "")
@@ -634,7 +637,7 @@
   (->> (db/get-all-pages repo)
        (remove (fn [p]
                  (let [name (:block/name p)]
-                   (or (gp-util/uuid-string? name)
+                   (or (util/uuid-string? name)
                        (gp-config/draw? name)
                        (db/built-in-pages-names (string/upper-case name))))))
        (common-handler/fix-pages-timestamps)))
@@ -688,7 +691,7 @@
               chosen (if (string/starts-with? chosen "New page: ") ;; FIXME: What if a page named "New page: XXX"?
                        (subs chosen 10)
                        chosen)
-              chosen (if (and (gp-util/safe-re-find #"\s+" chosen) (not wrapped?))
+              chosen (if (and (util/safe-re-find #"\s+" chosen) (not wrapped?))
                        (util/format "[[%s]]" chosen)
                        chosen)
               q (if @editor-handler/*selected-text "" q)
@@ -725,7 +728,7 @@
                (not (state/loading-files? repo)))
       (state/set-today! (date/today))
       (when (or (config/local-db? repo)
-                (and (= "local" repo) (not (mobile-util/is-native-platform?))))
+                (and (= "local" repo) (not (mobile-util/native-platform?))))
         (let [title (date/today)
               today-page (util/page-name-sanity-lc title)
               format (state/get-preferred-format repo)

+ 18 - 10
src/main/frontend/handler/plugin.cljs

@@ -3,7 +3,7 @@
             [rum.core :as rum]
             [frontend.util :as util]
             [clojure.walk :as walk]
-            [frontend.format.mldoc :as mldoc]
+            [logseq.graph-parser.mldoc :as gp-mldoc]
             [frontend.handler.notification :as notifications]
             [camel-snake-kebab.core :as csk]
             [frontend.state :as state]
@@ -364,9 +364,7 @@
   [pid]
   (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
     (when-let [theme (first themes)]
-      (let [theme-mode (:mode theme)]
-        (and theme-mode (state/set-theme! theme-mode))
-        (js/LSPluginCore.selectTheme (bean/->js theme))))))
+      (js/LSPluginCore.selectTheme (bean/->js theme)))))
 
 (defn update-plugin-settings-state
   [id settings]
@@ -403,7 +401,7 @@
                             (string/replace matched link (util/node-path.join url link))
                             matched)))
                       content)]
-        (format/to-html content :markdown (mldoc/default-config :markdown))))
+        (format/to-html content :markdown (gp-mldoc/default-config :markdown))))
     (catch js/Error e
       (log/error :parse-user-md-exception e)
       content)))
@@ -599,12 +597,22 @@
                                        (swap! state/state assoc :plugin/installed-themes
                                               (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
-                (.on "theme-selected" (fn [^js opts]
-                                        (let [opts (bean/->clj opts)
-                                              url (:url opts)
-                                              mode (:mode opts)]
-                                          (when mode (state/set-theme! mode))
+                (.on "theme-selected" (fn [^js theme]
+                                        (let [theme (bean/->clj theme)
+                                              url (:url theme)
+                                              mode (:mode theme)]
+                                          (when mode
+                                            (state/set-custom-theme! mode theme)
+                                            (state/set-theme-mode! mode))
                                           (state/set-state! :plugin/selected-theme url))))
+                                        
+                (.on "reset-custom-theme" (fn [^js themes]
+                                            (let [themes (bean/->clj themes)
+                                                  custom-theme (dissoc themes :mode)
+                                                  mode (:mode themes)]
+                                              (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
+                                                                        :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
+                                              (state/set-theme-mode! mode))))
 
                 (.on "settings-changed" (fn [id ^js settings]
                                           (let [id (keyword id)]

+ 4 - 4
src/main/frontend/handler/repo.cljs

@@ -5,7 +5,6 @@
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.format :as format]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
             [frontend.handler.common :as common-handler]
@@ -23,6 +22,7 @@
             [shadow.resource :as rc]
             [frontend.db.persist :as db-persist]
             [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.config :as gp-config]
             [electron.ipc :as ipc]
             [clojure.set :as set]
             [clojure.core.async :as async]
@@ -217,8 +217,8 @@
   [repo-url files delete-files delete-blocks file-paths db-encrypted? re-render? re-render-opts opts]
   (let [support-files (filter
                        (fn [file]
-                         (let [format (format/get-format (:file/path file))]
-                           (contains? (set/union #{:edn :css} config/mldoc-support-formats) format)))
+                         (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)
@@ -295,7 +295,7 @@
                      (common-handler/read-config content)))
         relate-path-fn (fn [m k]
                          (some-> (get m k)
-                                 (string/replace (str (config/get-local-dir repo-url) "/") "")))
+                                 (string/replace (js/decodeURI (config/get-local-dir repo-url)) "")))
         nfs-files (common-handler/remove-hidden-files nfs-files config #(relate-path-fn % :file/path))
         diffs (common-handler/remove-hidden-files diffs config #(relate-path-fn % :path))
         load-contents (fn [files option]

+ 5 - 6
src/main/frontend/handler/route.cljs

@@ -1,15 +1,14 @@
 (ns frontend.handler.route
   (:require [clojure.string :as string]
+            [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.search :as search-handler]
             [frontend.state :as state]
-            [frontend.text :as text]
+            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
-            [logseq.graph-parser.util :as gp-util]
-            [medley.core :as medley]
             [reitit.frontend.easy :as rfe]))
 
 (defn redirect!
@@ -78,11 +77,11 @@
     "Create a new page"
     :page
     (let [name (:name path-params)
-          block? (gp-util/uuid-string? name)]
+          block? (util/uuid-string? name)]
       (if block?
-        (if-let [block (db/entity [:block/uuid (medley/uuid name)])]
+        (if-let [block (db/entity [:block/uuid (uuid name)])]
           (let [content (text/remove-level-spaces (:block/content block)
-                                                  (:block/format block))]
+                                                  (:block/format block) (config/get-block-pattern (:block/format block)))]
             (if (> (count content) 48)
               (str (subs content 0 48) "...")
               content))

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