diff --git a/clj-e2e/dev/user.clj b/clj-e2e/dev/user.clj
index b42e4db3a5..b39c3a5931 100644
--- a/clj-e2e/dev/user.clj
+++ b/clj-e2e/dev/user.clj
@@ -15,6 +15,7 @@
[logseq.e2e.multi-tabs-basic-test]
[logseq.e2e.outliner-basic-test]
[logseq.e2e.plugins-basic-test]
+ [logseq.e2e.plugins-marketplace-test]
[logseq.e2e.property-basic-test]
[logseq.e2e.property-scoped-choices-test]
[logseq.e2e.reference-basic-test]
@@ -89,6 +90,11 @@
(->> (future (run-tests 'logseq.e2e.plugins-basic-test))
(swap! *futures assoc :plugins-test)))
+(defn run-plugins-marketplace-test
+ []
+ (->> (future (run-tests 'logseq.e2e.plugins-marketplace-test))
+ (swap! *futures assoc :plugins-marketplace-test)))
+
(defn run-rtc-extra-test
[]
(->> (future (run-tests 'logseq.e2e.rtc-extra-test))
@@ -131,38 +137,38 @@
(defn start
[]
(future
- (fixtures/open-page
- repl/pause
- {:headless false})))
+ (fixtures/open-page
+ repl/pause
+ {:headless false})))
(comment
- ;; You can call or put `(repl/pause)` in any test to pause the tests,
- ;; this allows us to continue experimenting with the current page.
- (repl/pause)
+ ;; You can call or put `(repl/pause)` in any test to pause the tests,
+ ;; this allows us to continue experimenting with the current page.
+ (repl/pause)
- ;; To resume the tests, close the page/context/browser
- (repl/resume)
+ ;; To resume the tests, close the page/context/browser
+ (repl/resume)
- ;; Run specific test
- (future (run-test logseq.e2e.commands-test/new-property-test))
+ ;; Run specific test
+ (future (run-test logseq.e2e.commands-test/new-property-test))
- ;; after the test has been paused, you can do anything with the current page like this
- (repl/with-page
- (w/wait-for (first (util/get-edit-block-container))
- {:state :detached}))
+ ;; after the test has been paused, you can do anything with the current page like this
+ (repl/with-page
+ (w/wait-for (first (util/get-edit-block-container))
+ {:state :detached}))
- (run-tests 'logseq.e2e.commands-basic-test
- 'logseq.e2e.multi-tabs-basic-test
- 'logseq.e2e.outliner-basic-test
- 'logseq.e2e.rtc-basic-test)
+ (run-tests 'logseq.e2e.commands-basic-test
+ 'logseq.e2e.multi-tabs-basic-test
+ 'logseq.e2e.outliner-basic-test
+ 'logseq.e2e.rtc-basic-test)
- (do
- (reset! config/*headless true)
- (reset! config/*slow-mo 10)
- (run-tests 'logseq.e2e.reference-basic-test)
- (dotimes [i 10]
- (run-tests 'logseq.e2e.reference-basic-test)))
+ (do
+ (reset! config/*headless true)
+ (reset! config/*slow-mo 10)
+ (run-tests 'logseq.e2e.reference-basic-test)
+ (dotimes [i 10]
+ (run-tests 'logseq.e2e.reference-basic-test)))
- ;;
- )
+ ;;
+ )
diff --git a/clj-e2e/test/logseq/e2e/plugins_marketplace_test.clj b/clj-e2e/test/logseq/e2e/plugins_marketplace_test.clj
new file mode 100644
index 0000000000..1fd6aed519
--- /dev/null
+++ b/clj-e2e/test/logseq/e2e/plugins_marketplace_test.clj
@@ -0,0 +1,90 @@
+(ns logseq.e2e.plugins-marketplace-test
+ (:require [logseq.e2e.fixtures :as fixtures]
+ [logseq.e2e.assert :as assert]
+ [logseq.e2e.keyboard :as k]
+ [logseq.e2e.util :as util]
+ [clojure.test :refer [deftest testing use-fixtures]]
+ [wally.main :as w]))
+
+(use-fixtures :once fixtures/open-page)
+(use-fixtures :each fixtures/new-logseq-page)
+
+(defn- open-plugins-dialog
+ "Opens the plugins dialog via the More menu"
+ []
+ (util/double-esc)
+ (w/click "button[title='More'] .ls-icon-dots")
+ (w/click ".ui__dropdown-menu-item:has-text('Plugins')")
+ (w/wait-for ".cp__plugins-page"))
+
+(defn- switch-to-marketplace
+ "Switches to the Marketplace tab in the plugins dialog"
+ []
+ (w/click "button:has-text('Marketplace')")
+ ;; Wait for plugins to load from marketplace
+ (w/wait-for ".cp__plugins-marketplace-cnt" {:timeout 15000}))
+
+(defn- search-plugin
+ "Search for a plugin by name"
+ [search-term]
+ (w/fill ".cp__plugins-page input[placeholder*='Search']" search-term))
+
+(defn- click-install-button
+ "Clicks the installation button for the first visible plugin card"
+ []
+ (let [install-btn (.first (w/-query ".cp__plugins-item-card .ctl a.btn:has-text('Install')"))]
+ (w/click install-btn)))
+
+(defn- wait-for-plugin-installed
+ "Waits for the plugin to show as installed"
+ []
+ (w/wait-for ".cp__plugins-item-card .ctl a.btn:has-text('Installed')" {:timeout 30000}))
+
+(defn- switch-to-installed
+ "Switches to the Installed tab in the plugins dialog"
+ []
+ (w/click "button:has-text('Installed')")
+ ;; Wait for installed plugins view
+ (w/wait-for ".cp__plugins-installed"))
+
+(defn- close-plugins-dialog
+ "Closes the plugins dialog"
+ []
+ (k/esc))
+
+(deftest install-plugin-from-marketplace
+ (testing "Install a plugin from the marketplace"
+ ;; Open plugins dialog
+ (open-plugins-dialog)
+
+ ;; Switch to marketplace tab
+ (switch-to-marketplace)
+
+ ;; Search for a specific plugin (using Journals calendar as it's a well-known plugin)
+ (search-plugin "Journals calendar")
+
+ ;; Wait for search results
+ (w/wait-for ".cp__plugins-item-card h3:has-text('Journals calendar')" {:timeout 10000})
+
+ ;; Click install on the plugin
+ (click-install-button)
+
+ ;; Wait for installation to complete
+ (wait-for-plugin-installed)
+
+ ;; Verify the plugin is now marked as installed in marketplace
+ (assert/assert-is-visible ".cp__plugins-item-card .ctl a.btn.disabled:has-text('Installed')")
+
+ ;; Switch to installed tab and verify plugin appears there
+ (switch-to-installed)
+ (assert/assert-is-visible ".cp__plugins-item-card h3:has-text('Journals calendar')")
+
+ ;; Close the dialog
+ (close-plugins-dialog)
+
+ ;; Check that the plugin is active in the main UI (e.g., check for a UI element added by the plugin
+ (w/wait-for ".toolbar-plugins-manager-trigger")
+ (w/click ".toolbar-plugins-manager-trigger")
+ (assert/assert-is-visible "a.button[data-on-click=goToToday]")
+ (w/click ".ui__dropdown-menu-content a.button[data-on-click=goToToday]")
+ (assert/assert-is-visible ".is-today-page")))
diff --git a/libs/development-notes/experiments_api_guide.md b/libs/development-notes/experiments_api_guide.md
new file mode 100644
index 0000000000..72329db74f
--- /dev/null
+++ b/libs/development-notes/experiments_api_guide.md
@@ -0,0 +1,460 @@
+# Logseq Experiments API Guide
+
+This guide covers the **experimental APIs** available in the Logseq Plugin SDK. These APIs provide advanced functionality for creating custom renderers, loading external scripts, and accessing internal utilities.
+
+> **⚠️ WARNING**: These are experimental features that may change at any time. Plugins using these APIs may not be supported on the Marketplace temporarily.
+
+---
+
+## Overview
+
+The Experiments API is accessed via `logseq.Experiments` and provides:
+
+1. **React Integration** - Access to React and ReactDOM from the host
+2. **Custom Renderers** - Register custom code block, route, and daemon renderers
+3. **Component Access** - Access to internal Logseq components
+4. **Utilities** - ClojureScript interop utilities (toClj, toJs, etc.)
+5. **Script Loading** - Dynamic loading of external scripts
+6. **Extension Enhancers** - Enhance libraries like KaTeX and CodeMirror
+
+---
+
+## 1. React Integration
+
+Access React and ReactDOM from the Logseq host environment.
+
+### Properties
+
+#### `logseq.Experiments.React`
+
+Returns the React instance from the host scope.
+
+```typescript
+const React = logseq.Experiments.React
+```
+
+#### `logseq.Experiments.ReactDOM`
+
+Returns the ReactDOM instance from the host scope.
+
+```typescript
+const ReactDOM = logseq.Experiments.ReactDOM
+```
+
+### Example Usage
+
+```typescript
+const React = logseq.Experiments.React
+const ReactDOM = logseq.Experiments.ReactDOM
+
+// Use React to create components
+const MyComponent = React.createElement('div', null, 'Hello from plugin!')
+```
+
+---
+
+## 2. Components
+
+Access internal Logseq components for advanced UI integration.
+
+### `logseq.Experiments.Components.Editor`
+
+A page editor component that can render Logseq page content.
+
+**Type**: `(props: { page: string } & any) => any`
+
+**Parameters**:
+- `page` (string): The page name to render
+
+```typescript
+const Editor = logseq.Experiments.Components.Editor
+
+// Render a page editor
+const editor = Editor({ page: 'My Page Name' })
+```
+
+---
+
+## 3. Utilities
+
+ClojureScript interop utilities for data conversion between JavaScript and ClojureScript.
+
+### `logseq.Experiments.Utils`
+
+Provides conversion utilities:
+
+#### `toClj(input: any)`
+
+Convert JavaScript data to ClojureScript data structures.
+
+```typescript
+const cljData = logseq.Experiments.Utils.toClj({ key: 'value' })
+```
+
+#### `jsxToClj(input: any)`
+
+Convert JSX/JavaScript objects to ClojureScript, preserving JSX structures.
+
+```typescript
+const cljData = logseq.Experiments.Utils.jsxToClj(
Content
)
+```
+
+#### `toJs(input: any)`
+
+Convert ClojureScript data structures to JavaScript.
+
+```typescript
+const jsData = logseq.Experiments.Utils.toJs(cljData)
+```
+
+#### `toKeyword(input: any)`
+
+Convert a string to a ClojureScript keyword.
+
+```typescript
+const keyword = logseq.Experiments.Utils.toKeyword('my-key')
+```
+
+#### `toSymbol(input: any)`
+
+Convert a string to a ClojureScript symbol.
+
+```typescript
+const symbol = logseq.Experiments.Utils.toSymbol('my-symbol')
+```
+
+---
+
+## 4. Script Loading
+
+### `logseq.Experiments.loadScripts(...scripts: string[])`
+
+Dynamically load external scripts into the Logseq environment.
+
+**Parameters**:
+- `scripts` (string[]): Array of script URLs or relative paths
+
+**Returns**: `Promise`
+
+**Behavior**:
+- Relative paths are resolved using the plugin's resource path
+- HTTP/HTTPS URLs are loaded directly
+- Scripts are loaded in order
+
+```typescript
+// Load external library
+await logseq.Experiments.loadScripts(
+ 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'
+)
+
+// Load local script from plugin resources
+await logseq.Experiments.loadScripts('./my-script.js')
+
+// Load multiple scripts
+await logseq.Experiments.loadScripts(
+ 'https://cdn.example.com/lib1.js',
+ 'https://cdn.example.com/lib2.js',
+ './local-script.js'
+)
+```
+
+---
+
+## 5. Custom Renderers
+
+### 5.1 Fenced Code Renderer
+
+Register a custom renderer for code blocks with specific language tags.
+
+#### `logseq.Experiments.registerFencedCodeRenderer(lang: string, opts: object)`
+
+**Parameters**:
+- `lang` (string): The language identifier for the code block (e.g., 'mermaid', 'chart')
+- `opts` (object):
+ - `render` (function, required): Render function that receives props
+ - `edit` (boolean, optional): Whether the block is editable
+ - `before` (function, optional): Async function to run before rendering
+ - `subs` (string[], optional): Subscriptions to state changes
+
+**Render Props**:
+- `content` (string): The content of the code block
+
+```typescript
+// Register a custom code block renderer
+logseq.Experiments.registerFencedCodeRenderer('my-chart', {
+ edit: false,
+ before: async () => {
+ // Load dependencies before rendering
+ await logseq.Experiments.loadScripts(
+ 'https://cdn.jsdelivr.net/npm/chart.js'
+ )
+ },
+ render: (props) => {
+ const React = logseq.Experiments.React
+
+ return React.createElement('div', {
+ ref: (el) => {
+ if (el) {
+ // Parse content and render chart
+ const config = JSON.parse(props.content)
+ new Chart(el, config)
+ }
+ }
+ })
+ }
+})
+```
+
+**Usage in Logseq**:
+````markdown
+```my-chart
+{
+ "type": "bar",
+ "data": {
+ "labels": ["A", "B", "C"],
+ "datasets": [{"data": [10, 20, 30]}]
+ }
+}
+```
+````
+
+### 5.2 Daemon Renderer
+
+Register a renderer that runs continuously in the background (daemon).
+
+#### `logseq.Experiments.registerDaemonRenderer(key: string, opts: object)`
+
+**Parameters**:
+- `key` (string): Unique identifier for the daemon renderer
+- `opts` (object):
+ - `render` (function, required): Render function
+ - `sub` (string[], optional): Subscriptions to state changes
+
+```typescript
+// Register a daemon renderer for persistent UI
+logseq.Experiments.registerDaemonRenderer('my-status-bar', {
+ sub: ['ui/theme', 'ui/sidebar-open'],
+ render: (props) => {
+ const React = logseq.Experiments.React
+
+ return React.createElement('div', {
+ style: {
+ position: 'fixed',
+ bottom: 0,
+ right: 0,
+ padding: '10px',
+ background: '#333',
+ color: '#fff'
+ }
+ }, 'Status: Active')
+ }
+})
+```
+
+### 5.3 Route Renderer
+
+Register a custom renderer for specific routes in Logseq.
+
+#### `logseq.Experiments.registerRouteRenderer(key: string, opts: object)`
+
+**Parameters**:
+- `key` (string): Unique identifier for the route renderer
+- `opts` (object):
+ - `path` (string, required): Route path (e.g., '/my-plugin-page')
+ - `render` (function, required): Render function
+ - `name` (string, optional): Display name for the route
+ - `subs` (string[], optional): Subscriptions to state changes
+
+```typescript
+// Register a custom route
+logseq.Experiments.registerRouteRenderer('my-custom-page', {
+ path: '/my-plugin-dashboard',
+ name: 'Dashboard',
+ subs: ['ui/theme'],
+ render: (props) => {
+ const React = logseq.Experiments.React
+
+ return React.createElement('div', {
+ className: 'my-plugin-dashboard'
+ }, [
+ React.createElement('h1', null, 'Plugin Dashboard'),
+ React.createElement('p', null, 'Custom content here')
+ ])
+ }
+})
+
+// Navigate to the route
+logseq.App.pushState('page', { name: 'my-plugin-dashboard' })
+```
+
+---
+
+## 6. Extension Enhancers
+
+Enhance external libraries that Logseq uses (like KaTeX for math rendering).
+
+### `logseq.Experiments.registerExtensionsEnhancer(type: string, enhancer: function)`
+
+**Parameters**:
+- `type` ('katex' | 'codemirror'): The extension type to enhance
+- `enhancer` (function): Async function that receives the library instance and can modify it
+
+**Returns**: `Promise`
+
+```typescript
+// Enhance KaTeX with custom macros
+logseq.Experiments.registerExtensionsEnhancer('katex', async (katex) => {
+ // Add custom KaTeX macros
+ katex.macros = {
+ ...katex.macros,
+ '\\RR': '\\mathbb{R}',
+ '\\NN': '\\mathbb{N}',
+ '\\ZZ': '\\mathbb{Z}'
+ }
+
+ console.log('KaTeX enhanced with custom macros')
+})
+```
+
+---
+
+## 7. Plugin Local Access
+
+### `logseq.Experiments.pluginLocal`
+
+Access the internal plugin instance (PluginLocal) for advanced operations.
+
+**Type**: `PluginLocal`
+
+```typescript
+const pluginLocal = logseq.Experiments.pluginLocal
+
+// Access plugin-specific internal state
+console.log('Plugin ID:', pluginLocal.id)
+```
+
+---
+
+## 8. Advanced: Invoke Experimental Methods
+
+### `logseq.Experiments.invokeExperMethod(type: string, ...args: any[])`
+
+Directly invoke experimental methods from the host scope.
+
+**Parameters**:
+- `type` (string): Method name (converted to snake_case)
+- `...args`: Arguments to pass to the method
+
+**Returns**: `any`
+
+```typescript
+// Invoke a custom experimental method
+const result = logseq.Experiments.invokeExperMethod(
+ 'someExperimentalFeature',
+ arg1,
+ arg2
+)
+```
+
+---
+
+## Complete Example: Custom Chart Renderer
+
+Here's a complete example combining multiple APIs:
+
+```typescript
+import '@logseq/libs'
+
+async function main() {
+ console.log('Chart Plugin Loaded')
+
+ // Register fenced code renderer for charts
+ logseq.Experiments.registerFencedCodeRenderer('chart', {
+ edit: false,
+ before: async () => {
+ // Load Chart.js before rendering
+ await logseq.Experiments.loadScripts(
+ 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'
+ )
+ },
+ render: (props) => {
+ const React = logseq.Experiments.React
+
+ return React.createElement('div', null, [
+ React.createElement('canvas', {
+ ref: (canvas) => {
+ if (canvas && window.Chart) {
+ try {
+ const config = JSON.parse(props.content)
+ new window.Chart(canvas, config)
+ } catch (e) {
+ console.error('Chart rendering error:', e)
+ }
+ }
+ }
+ })
+ ])
+ }
+ })
+}
+
+logseq.ready(main).catch(console.error)
+```
+
+**Usage**:
+````markdown
+```chart
+{
+ "type": "line",
+ "data": {
+ "labels": ["Jan", "Feb", "Mar", "Apr"],
+ "datasets": [{
+ "label": "Sales",
+ "data": [10, 20, 15, 30],
+ "borderColor": "rgb(75, 192, 192)"
+ }]
+ }
+}
+```
+````
+
+---
+
+## Best Practices
+
+1. **Check Host Scope**: Always ensure the host scope is accessible before using experimental APIs
+2. **Error Handling**: Wrap experimental API calls in try-catch blocks
+3. **Dependencies**: Load external scripts in `before` hooks to ensure they're ready
+4. **Memory Management**: Clean up event listeners and subscriptions in daemon renderers
+5. **Compatibility**: Test thoroughly as these APIs may change between Logseq versions
+6. **Documentation**: Document which experimental APIs your plugin uses
+7. **Marketplace**: Be aware that plugins using these APIs may not be accepted on the Marketplace
+
+---
+
+## Limitations
+
+- **Experimental Status**: These APIs are not stable and may change without notice
+- **Marketplace Support**: Plugins using experimental APIs may not be approved for the Marketplace
+- **Security**: Be cautious when loading external scripts or accessing host scope
+- **Performance**: Custom renderers can impact performance if not optimized
+- **Compatibility**: Limited backwards compatibility guarantees
+
+---
+
+## See Also
+
+- [Starter Guide](./starter_guide.md) - Getting started with plugin development
+- [DB Properties Guide](./db_properties_guide.md) - Working with database properties
+- [DB Query Guide](./db_query_guide.md) - Querying the Logseq database
+
+---
+
+## Support
+
+For questions and issues:
+- [Logseq Discord](https://discord.gg/logseq) - #plugin-dev channel
+- [GitHub Discussions](https://github.com/logseq/logseq/discussions)
+- [Plugin API Documentation](https://plugins-doc.logseq.com/)
+
+Remember: These are experimental features. Use at your own risk and always test thoroughly!
diff --git a/libs/development-notes/starter_guide.md b/libs/development-notes/starter_guide.md
new file mode 100644
index 0000000000..cf1aed3fb1
--- /dev/null
+++ b/libs/development-notes/starter_guide.md
@@ -0,0 +1,125 @@
+# Logseq Plugin Starter Guide
+
+# For Developers
+
+In this short guide, it will walk you through the steps needed to set up your development environment for writing and
+running a hello world simple inside of Logseq Desktop Client. You know Logseq Plugin based on Web Technologies composed
+of JS & HTML & CSS, but neither are mandatory, you can develop plugins use any language which can be translated to
+javascript (Logseq made by Clojurescript!). We will use Typescript to demonstrate this sample. Because there
+is [a type definition file](https://www.npmjs.com/package/@logseq/libs) to make development experience even better.
+
+## Install Node.js and NPM
+
+You can download Node.js here, which will include NPM: https://nodejs.org/en/download/.
+
+## Install TypeScript
+
+To install TypeScript, run `npm install -g typescript` in a terminal.
+
+## Get Logseq desktop app
+
+At this time, plugin development and testing needs to be done using the Logseq desktop app. This is because Logseq needs
+to read your code saved as a local file. The Logseq desktop app can be downloaded
+here: https://github.com/logseq/logseq/releases.
+
+If you already have the desktop app, please make sure to update to the latest version, as several features have been
+added specifically in order to provide a better plugin development experience.
+
+## Go to Settings > Turn on Developer Mode
+
+This will bring up the "Plugins" entry in three dots more menu list on top right of head bar. Go to Plugins page, and
+you will get a button with `Load unpacked plugin` label.
+
+## Create plugin package from scaffold
+
+We will use [logseq-hello-world](https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-hello-world) as
+scaffold. Download this package files using Git or download zip straight to your own plugin package directory.
+
+## Explain `package.json`
+
+Generally Logseq plugin package is described by `package.json` file located root. Let's checkout these primary keys.
+
+```json
+{
+ "name": "logseq-plugin-hello-world",
+ "version": "0.0.1",
+ "main": "dist/index.html",
+ "logseq": {
+ "id": "yet-another-hello-world-plugin",
+ "title": "Say Hi to Logseq",
+ "icon": "logseq.png"
+ }
+}
+```
+
+- __name__ [required] a name of plugin.
+ - it starts with `logseq-plugin` is better for Googling.
+- __version__ [required] semantic version
+ - related link: https://semver.org/
+- __main__ [optional] entry of plugin, optional for theme plugin only.
+ - `.html` file entry indicate that the plugin provide main iframe ui.
+ - `.js` file entry indicate that the plugin without main iframe ui.
+- __logseq__ [required] specs of plugin & indicate it's a Logseq package.
+ - `id` [optional] the identity key of plugin. if you don't provide it that will be auto generated on load
+ development package.
+ - `title` [optional] the title display of plugin list card. if not provided, value of `pkg#name` would be used.
+ - `icon` [optional] plugin logo for your own brand :D.
+ - `minSDKVersion` - [optional] the minimum sdk version what your plugin required. eg: `0.1.0`.
+ - `supportsDB` - [optional] the boolean value indicate whether your plugin support the database feature.
+ - `supportsDBOnly` - [optional] the boolean value indicate whether your plugin only support the database feature.
+
+ Apart from these primary keys, you might find other useful fields, e.g. `author`, `description`, `repository`.
+ Although these are not necessary, but completed fields are kind for your plugin users.
+
+## Develop your code
+
+This scaffold using [Parcel](https://v2.parceljs.org/) as `ts` bundler, just it works out of box. Of Course, you could
+choose another one what you prefer. Then install dependencies `npm install` and watch your code in development mode
+`npm run dev`. It will build the development version outputs to the dist directory. Go to plugins page and pick your
+plugin root directory by `Load unpacked plugin` button. You will get a message from this plugin. Let's explain the entry
+file of `index.ts`.
+
+### 1. import user plugin SDK [required]
+
+```js
+import '@logseq/libs'
+```
+
+It provides TS types and global sdk namespace `logseq`.
+
+### 2. bootstrap your main function [required]
+
+```js
+logseq.ready(main).catch(console.error)
+```
+
+Limited by the plugin system mechanism, user main function should wait for some preparations to make sure the SDK api
+works correctly!
+
+### 3. show your code [required]
+
+```js
+function main () {
+ logseq.UI.showMsg('❤️ Message from Hello World Plugin :)')
+}
+```
+
+Change your code and reload the plugin to make it works.
+
+> If you develop main ui in iframe, [HMR](https://webpack.js.org/concepts/hot-module-replacement/) feature provided by
+> some ui frameworks is convenient that can avoid reloading plugin again and again.
+
+🎉 Actually, you're done!
+
+## Next steps
+
+- Awesome samples
+ - https://github.com/logseq/logseq-plugin-samples
+ - https://github.com/logseq/awesome-logseq
+- Community plugins
+ - https://github.com/search?q=logseq-plugin
+
+# For users
+
+Currently, Logseq plugin can only run in developer mode. Under this mode, plugin has ability to access notes data. So if
+you're working with sensitive data, you'd better confirm the plugin is from trusted resource before installing it.
\ No newline at end of file
diff --git a/libs/package.json b/libs/package.json
index 436112e6e6..2967d94109 100644
--- a/libs/package.json
+++ b/libs/package.json
@@ -1,6 +1,6 @@
{
"name": "@logseq/libs",
- "version": "0.2.12",
+ "version": "0.3.1",
"description": "Logseq SDK libraries",
"main": "dist/lsplugin.user.js",
"typings": "index.d.ts",
diff --git a/libs/src/LSPlugin.caller.ts b/libs/src/LSPlugin.caller.ts
index aaad807acf..718250c150 100644
--- a/libs/src/LSPlugin.caller.ts
+++ b/libs/src/LSPlugin.caller.ts
@@ -2,7 +2,7 @@ import Debug from 'debug'
import { Postmate, Model, ParentAPI, ChildAPI } from './postmate'
import EventEmitter from 'eventemitter3'
import { PluginLocal } from './LSPlugin.core'
-import { deferred, IS_DEV } from './helpers'
+import { deferred, IS_DEV } from './common'
import { LSPluginShadowFrame } from './LSPlugin.shadow'
const debug = Debug('LSPlugin:caller')
@@ -19,6 +19,9 @@ export const LSPMSG_READY = '#lspmsg#ready#'
export const LSPMSGFn = (id: string) => `${LSPMSG}${id}`
export const AWAIT_LSPMSGFn = (id: string) => `${FLAG_AWAIT}${id}`
+const HEAD_BAR_HEIGHT = 45
+const MAX_LAYOUT_PERCENT = 99
+
/**
* Call between core and user
*/
@@ -32,11 +35,12 @@ class LSPluginCaller extends EventEmitter {
private _status?: 'pending' | 'timeout'
private _userModel: any = {}
+ private _syncGCTimer: any = null
private _call?: (
type: string,
payload: any,
- actor?: DeferredActor
+ actor?: DeferredActor,
) => Promise
private _callUserModel?: (type: string, ...payloads: any[]) => Promise
@@ -70,13 +74,12 @@ class LSPluginCaller extends EventEmitter {
const caller = this
const isShadowMode = this._pluginLocal != null
- let syncGCTimer: any = 0
let syncTag = 0
const syncActors = new Map()
const readyDeferred = deferred(1000 * 60)
const model: any = this._extendUserModel({
- [LSPMSG_READY]: async (baseInfo) => {
+ [LSPMSG_READY]: async (baseInfo: any) => {
// dynamically setup common msg handler
model[LSPMSGFn(baseInfo?.pid)] = ({
type,
@@ -99,14 +102,14 @@ class LSPluginCaller extends EventEmitter {
await actor.promise
},
- [LSPMSG_SETTINGS]: async ({ type, payload }) => {
+ [LSPMSG_SETTINGS]: async ({ payload }) => {
caller.emit('settings:changed', payload)
},
[LSPMSG]: async ({ ns, type, payload }: any) => {
debug(
`[host (async) -> *user] ${this._debugTag} ns=${ns} type=${type}`,
- payload
+ payload,
)
if (ns && ns.startsWith('hook')) {
@@ -149,8 +152,8 @@ class LSPluginCaller extends EventEmitter {
this._status = 'pending'
await handshake
- .then((refParent: ChildAPI) => {
- this._child = refParent
+ .then((childRefParent: ChildAPI) => {
+ this._child = childRefParent
this._connected = true
this._call = async (type, payload = {}, actor) => {
@@ -163,7 +166,7 @@ class LSPluginCaller extends EventEmitter {
debug(`async call #${tag}`)
}
- refParent.emit(LSPMSGFn(model.baseInfo.id), { type, payload })
+ childRefParent.emit(LSPMSGFn(model.baseInfo.id), { type, payload })
return actor?.promise as Promise
}
@@ -172,12 +175,12 @@ class LSPluginCaller extends EventEmitter {
try {
model[type](payload)
} catch (e) {
- debug(`[model method] #${type} not existed`)
+ debug(`call user model(${type}) not exist. #${this._debugTag}`)
}
}
// actors GC
- syncGCTimer = setInterval(() => {
+ this._syncGCTimer = setInterval(() => {
if (syncActors.size > 100) {
for (const [k, v] of syncActors) {
if (v.settled) {
@@ -215,6 +218,40 @@ class LSPluginCaller extends EventEmitter {
return this._callUserModel?.apply(this, [type, ...args])
}
+ /**
+ * Converts a raw pixel position to a percentage or pixel CSS value,
+ * clamping the value to [minValue, MAX_LAYOUT_PERCENT%].
+ */
+ private _calcLayoutPosition(
+ value: number,
+ viewportSize: number | undefined,
+ minValue: number = 0,
+ ): string {
+ const clamped = Math.max(value, minValue)
+ if (typeof viewportSize === 'number') {
+ return `${Math.min((clamped * 100) / viewportSize, MAX_LAYOUT_PERCENT)}%`
+ }
+ return `${clamped}px`
+ }
+
+ /**
+ * Restores persisted layout data onto the container element.
+ */
+ private async _applyContainerLayout(cnt: HTMLDivElement): Promise {
+ const mainLayoutInfo = (await this._pluginLocal?._loadLayoutsData())?.$$0
+ if (!mainLayoutInfo) return
+
+ const { width, height, left, top, vw, vh } = mainLayoutInfo
+
+ cnt.dataset.inited_layout = 'true'
+ Object.assign(cnt.style, {
+ width: `${width}px`,
+ height: `${height}px`,
+ left: this._calcLayoutPosition(left, vw),
+ top: this._calcLayoutPosition(top, vh, HEAD_BAR_HEIGHT),
+ })
+ }
+
// run in host
async _setupIframeSandbox() {
const pl = this._pluginLocal!
@@ -224,45 +261,19 @@ class LSPluginCaller extends EventEmitter {
url.searchParams.set(
`__v__`,
- IS_DEV ? Date.now().toString() : pl.options.version
+ IS_DEV ? Date.now().toString() : pl.options.version,
)
// clear zombie sandbox
- const zb = document.querySelector(`#${domId}`)
- if (zb) zb.parentElement.removeChild(zb)
+ document.querySelector(`#${domId}`)?.remove()
const cnt = document.createElement('div')
cnt.classList.add('lsp-iframe-sandbox-container')
cnt.id = domId
cnt.dataset.pid = id
- // TODO: apply any container layout data
try {
- const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
- if (mainLayoutInfo) {
- cnt.dataset.inited_layout = 'true'
- let { width, height, left, top, vw, vh } = mainLayoutInfo
-
- left = Math.max(left, 0)
- left =
- typeof vw === 'number'
- ? `${Math.min((left * 100) / vw, 99)}%`
- : `${left}px`
-
- // 45 is height of headbar
- top = Math.max(top, 45)
- top =
- typeof vh === 'number'
- ? `${Math.min((top * 100) / vh, 99)}%`
- : `${top}px`
-
- Object.assign(cnt.style, {
- width: width + 'px',
- height: height + 'px',
- left,
- top,
- })
- }
+ await this._applyContainerLayout(cnt)
} catch (e) {
console.error('[Restore Layout Error]', e)
}
@@ -276,70 +287,66 @@ class LSPluginCaller extends EventEmitter {
classListArray: ['lsp-iframe-sandbox'],
model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) },
allow: pl.options.allow,
+ // for optimized postmate message
+ enableMessageChannel: true,
})
- let handshake = pt.sendHandshake()
this._status = 'pending'
- // timeout for handshake
- let timer
+ const HANDSHAKE_TIMEOUT = 8_000
+ let timer: ReturnType
- return new Promise((resolve, reject) => {
+ const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
- reject(new Error(`handshake Timeout`))
pt.destroy()
- }, 8 * 1000) // 8 secs
-
- handshake
- .then((refChild: ParentAPI) => {
- this._parent = refChild
- this._connected = true
- this.emit('connected')
-
- refChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
- debug(`[user -> *host] `, type, payload)
-
- this._pluginLocal?.emit(type, payload || {})
- this._pluginLocal?.caller.emit(type, payload || {})
- })
-
- this._call = async (...args: any) => {
- // parent all will get message before handshake
- refChild.call(LSPMSGFn(pl.id), {
- type: args[0],
- payload: Object.assign(args[1] || {}, {
- $$pid: pl.id,
- }),
- })
- }
-
- this._callUserModel = async (type, ...payloads: any[]) => {
- if (type.startsWith(FLAG_AWAIT)) {
- return await refChild.get(
- type.replace(FLAG_AWAIT, ''),
- ...payloads
- )
- } else {
- refChild.call(type, payloads?.[0])
- }
- }
-
- resolve(null)
- })
- .catch((e) => {
- reject(e)
- })
- .finally(() => {
- clearTimeout(timer)
- })
+ reject(new Error('handshake Timeout'))
+ }, HANDSHAKE_TIMEOUT)
})
- .catch((e) => {
- debug('[iframe sandbox] error', e)
- throw e
- })
- .finally(() => {
- this._status = undefined
+
+ try {
+ const parentRefChild = await Promise.race([
+ pt.sendHandshake(),
+ timeout,
+ ]) as ParentAPI
+
+ this._parent = parentRefChild
+ this._connected = true
+ this.emit('connected')
+
+ parentRefChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
+ debug(`[user -> *host] `, type, payload)
+
+ this._pluginLocal?.emit(type, payload || {})
+ this._pluginLocal?.caller.emit(type, payload || {})
})
+
+ this._call = async (...args: any) => {
+ // parent all will get message before handshake
+ parentRefChild.call(LSPMSGFn(pl.id), {
+ type: args[0],
+ payload: Object.assign(args[1] || {}, {
+ $$pid: pl.id,
+ }),
+ })
+ }
+
+ this._callUserModel = async (type, ...payloads: any[]) => {
+ if (type.startsWith(FLAG_AWAIT)) {
+ return await parentRefChild.get(
+ type.replace(FLAG_AWAIT, ''),
+ ...payloads,
+ )
+ } else {
+ parentRefChild.call(type, payloads?.[0])
+ }
+ }
+ } catch (e) {
+ debug('[iframe sandbox] error', e)
+ throw e
+ } finally {
+ clearTimeout(timer!)
+ this._status = undefined
+ }
}
async _setupShadowSandbox() {
@@ -362,7 +369,7 @@ class LSPluginCaller extends EventEmitter {
type,
Object.assign(payload, {
$$pid: pl.id,
- })
+ }),
)
return actor?.promise
@@ -399,7 +406,7 @@ class LSPluginCaller extends EventEmitter {
}
_getSandboxShadowContainer() {
- return this._shadow?.frame.parentNode as HTMLDivElement
+ return this._shadow?.frame as HTMLDivElement
}
_getSandboxIframeRoot() {
@@ -418,7 +425,7 @@ class LSPluginCaller extends EventEmitter {
let root: HTMLElement = null
if (this._parent) {
root = this._getSandboxIframeContainer()
- await this._parent.destroy()
+ this._parent.destroy()
}
if (this._shadow) {
@@ -426,8 +433,24 @@ class LSPluginCaller extends EventEmitter {
this._shadow.destroy()
}
- root?.parentNode.removeChild(root)
+ root?.parentNode?.removeChild(root)
+
+ // clear GC timer
+ if (this._syncGCTimer) {
+ clearInterval(this._syncGCTimer)
+ this._syncGCTimer = null
+ }
+
+ this._connected = false
+ this._parent = undefined
+ this._child = undefined
+ this._shadow = undefined
+ this._call = undefined
+ this._callUserModel = undefined
+ this._status = undefined
}
}
-export { LSPluginCaller }
+export {
+ LSPluginCaller
+}
diff --git a/libs/src/LSPlugin.core.ts b/libs/src/LSPlugin.core.ts
index c4ddf0cd00..f06d44ec4a 100644
--- a/libs/src/LSPlugin.core.ts
+++ b/libs/src/LSPlugin.core.ts
@@ -21,8 +21,8 @@ import {
injectTheme,
cleanInjectedUI,
PluginLogger,
-} from './helpers'
-import * as pluginHelpers from './helpers'
+} from './common'
+import * as pluginHelpers from './common'
import DOMPurify from 'dompurify'
import Debug from 'debug'
import {
@@ -86,25 +86,44 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
return this._settings[k]
}
- set(k: string | Record, v?: any) {
+ set(k: string, v?: any) {
const o = deepMerge({}, this._settings)
- if (typeof k === 'string') {
- if (this._settings[k] == v) return
- this._settings[k] = v
- } else if (isObject(k)) {
- this._settings = deepMerge(this._settings, k)
- } else {
+ if (this._settings[k] === v) {
return
}
+ this._settings = {
+ ...this._settings,
+ [k]: v,
+ }
+
+ this.emit('change', { ...this._settings }, o)
+ }
+
+ patch(input: Record) {
+ if (!isObject(input)) {
+ return
+ }
+
+ const o = deepMerge({}, this._settings)
+ this._settings = deepMerge(this._settings, input)
+
+ this.emit('change', { ...this._settings }, o)
+ }
+
+ replace(value: Record) {
+ const o = deepMerge({}, this._settings)
+ this._settings = {
+ disabled: false,
+ ...(value || {}),
+ }
+
this.emit('change', { ...this._settings }, o)
}
set settings(value: Record) {
- const o = deepMerge({}, this._settings)
- this._settings = value || {}
- this.emit('change', { ...this._settings }, o)
+ this.replace(value)
}
get settings(): Record {
@@ -115,9 +134,7 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
this._schema = schema
if (syncSettings) {
- const _settings = this._settings
- this._settings = mergeSettingsWithSchema(_settings, schema)
- this.emit('change', this._settings, _settings)
+ this.replace(mergeSettingsWithSchema(this._settings, schema))
}
}
@@ -129,7 +146,7 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
// TODO: generated by schema
}
- this.settings = val
+ this.replace(val)
this.emit('reset', val, o)
}
@@ -178,6 +195,15 @@ type RegisterPluginOpts = PluginLocalOptions | PluginLocalUrl
type PluginLocalIdentity = string
+interface MainUILayoutData {
+ width: number
+ height: number
+ left: number
+ top: number
+ vw: number
+ vh: number
+}
+
enum PluginLocalLoadStatus {
LOADING = 'loading',
UNLOADING = 'unloading',
@@ -201,7 +227,7 @@ function initUserSettingsHandlers(pluginLocal: PluginLocal) {
// settings:update
pluginLocal.on(_('update'), (attrs) => {
if (!attrs) return
- pluginLocal.settings?.set(attrs)
+ pluginLocal.settings?.patch(attrs)
})
// settings:visible:changed
@@ -361,7 +387,11 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) {
if (pluginLocal.shadow) {
if (payload.actor) {
- payload.actor.resolve(ret)
+ if (ret?.hasOwnProperty(LSPMSG_ERROR_TAG)) {
+ payload.actor.reject(ret[LSPMSG_ERROR_TAG])
+ } else {
+ payload.actor.resolve(ret)
+ }
}
return
}
@@ -392,9 +422,16 @@ function convertToLSPResource(fullUrl: string, dotPluginRoot: string) {
}
class IllegalPluginPackageError extends Error {
- constructor(message: string) {
+ url?: string
+ packageJsonPath?: string
+
+ constructor(
+ message: string,
+ options: Partial> = {}
+ ) {
super(message)
this.name = 'IllegalPluginPackageError'
+ Object.assign(this, options)
}
}
@@ -412,7 +449,8 @@ class PluginLocal extends EventEmitter<
'loaded' | 'unloaded' | 'beforeunload' | 'error' | string
> {
private _sdk: Partial = {}
- private _disposes: Array<() => Promise> = []
+ private _runtimeDisposes: Array<() => Promise> = []
+ private _registrationDisposes: Array<() => Promise> = []
private _id: PluginLocalIdentity
private _status: PluginLocalLoadStatus = PluginLocalLoadStatus.UNLOADED
private _loadErr?: Error
@@ -420,6 +458,7 @@ class PluginLocal extends EventEmitter<
private _dotSettingsFile?: string
private _caller?: LSPluginCaller
private _logger?: PluginLogger = new PluginLogger('PluginLocal')
+ private _disposeSettingsObserver?: () => void
/**
* @param _options
@@ -435,6 +474,11 @@ class PluginLocal extends EventEmitter<
this._id = _options.key || genID()
+ this._disposeRegistration(async () => {
+ this._disposeSettingsObserver?.()
+ this._disposeSettingsObserver = undefined
+ })
+
initUserSettingsHandlers(this)
initMainUIHandlers(this)
initProviderHandlers(this)
@@ -445,7 +489,7 @@ class PluginLocal extends EventEmitter<
const { _options } = this
const logger = (this._logger = new PluginLogger(`Loader:${this.debugTag}`))
- if (_options.settings && !reload) {
+ if (_options.settings && !reload && this._disposeSettingsObserver) {
return
}
@@ -461,31 +505,16 @@ class PluginLocal extends EventEmitter<
settings = _options.settings = new PluginSettings(userSettings)
}
+ this._disposeSettingsObserver?.()
+ this._disposeSettingsObserver = undefined
+
if (reload) {
- settings.settings = userSettings
- return
+ settings.replace(userSettings)
}
- const handler = async (a, b) => {
+ const handler = async (a) => {
debug('Settings changed', this.debugTag, a)
- if (!a.disabled && b.disabled) {
- // Enable plugin
- const [, freshSettings] = await loadFreshSettings()
- freshSettings.disabled = false
- a = Object.assign(a, freshSettings)
- settings.settings = a
- await this.load()
- }
-
- if (a.disabled && !b.disabled) {
- // Disable plugin
- const [, freshSettings] = await loadFreshSettings()
- freshSettings.disabled = true
- a = Object.assign(a, freshSettings)
- await this.unload()
- }
-
if (a) {
invokeHostExportedApi('save_plugin_user_settings', this.id, a)
}
@@ -494,7 +523,14 @@ class PluginLocal extends EventEmitter<
// observe settings
settings.on('change', handler)
- return () => {}
+ const disposeSettingsObserver = () => {
+ settings.off('change', handler)
+ if (this._disposeSettingsObserver === disposeSettingsObserver) {
+ this._disposeSettingsObserver = undefined
+ }
+ }
+
+ this._disposeSettingsObserver = disposeSettingsObserver
} catch (e) {
debug('[load plugin user settings Error]', e)
logger?.error(e)
@@ -519,7 +555,8 @@ class PluginLocal extends EventEmitter<
return `${this.installedFromUserWebUrl}/${filePath}`
}
- return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(localRoot, filePath)}`
+ return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(
+ localRoot, filePath)}`
}
const reg = /^(http|file)/
@@ -537,20 +574,35 @@ class PluginLocal extends EventEmitter<
let pkg: any = webPkg
if (!pkg) {
- try {
- if (!url) {
- throw new Error('Can not resolve package config location')
- }
+ let packageConfigError: string | undefined
+ if (!url) {
+ packageConfigError = 'Can not resolve package config location'
+ } else {
debug('prepare package root', url)
- pkg = await invokeHostExportedApi('load_plugin_config', url)
+ try {
+ pkg = await invokeHostExportedApi('load_plugin_config', url)
- if (!pkg || ((pkg = JSON.parse(pkg)), !pkg)) {
- throw new Error(`Parse package config error #${url}/package.json`)
+ if (!pkg) {
+ packageConfigError = `Parse package config error #${url}/package.json`
+ } else {
+ pkg = JSON.parse(pkg)
+
+ if (!pkg) {
+ packageConfigError = `Parse package config error #${url}/package.json`
+ }
+ }
+ } catch (e: any) {
+ packageConfigError = e?.message || String(e)
}
- } catch (e) {
- throw new IllegalPluginPackageError(e.message)
+ }
+
+ if (packageConfigError) {
+ throw new IllegalPluginPackageError(packageConfigError, {
+ url,
+ packageJsonPath: url ? path.join(url, 'package.json') : undefined,
+ })
}
}
@@ -572,7 +624,8 @@ class PluginLocal extends EventEmitter<
})
const { repo, version } = this._options
- const localRoot = (this._localRoot = this.isWebPlugin ? `${repo || url}/${version}` : safetyPathNormalize(url))
+ const localRoot = (this._localRoot = this.isWebPlugin ? `${repo ||
+ url}/${version}` : safetyPathNormalize(url))
const logseq: Partial = pkg.logseq || {}
// const validateEntry = (main) => main && /\.(js|html)$/.test(main)
@@ -669,9 +722,9 @@ class PluginLocal extends EventEmitter<
logseq plugin entry
${
- IS_DEV
- ? ``
- : ``
+ this.isWebPlugin
+ ? ``
+ : ``
}
@@ -719,17 +772,12 @@ class PluginLocal extends EventEmitter<
return layouts || {}
}
- async _saveLayoutsData(data) {
+ async _saveLayoutsData(data: any) {
const key = this.id + '_layouts'
await invokeHostExportedApi('save_plugin_user_settings', key, data)
}
- async _persistMainUILayoutData(e: {
- width: number
- height: number
- left: number
- top: number
- }) {
+ async _persistMainUILayoutData(e: MainUILayoutData) {
const layouts = await this._loadLayoutsData()
layouts.$$0 = e
await this._saveLayoutsData(layouts)
@@ -852,11 +900,14 @@ class PluginLocal extends EventEmitter<
reload: boolean
}>
) {
- if (this.pending) {
+ if (this.pending || this.loaded) {
return
}
- this._status = PluginLocalLoadStatus.LOADING
+ this._transitionStatus(PluginLocalLoadStatus.LOADING, [
+ PluginLocalLoadStatus.UNLOADED,
+ PluginLocalLoadStatus.ERROR,
+ ])
this._loadErr = undefined
try {
@@ -865,7 +916,7 @@ class PluginLocal extends EventEmitter<
const installPackageThemes = await this._preparePackageConfigs()
- this._dispose(await this._setupUserSettings(opts?.reload))
+ await this._setupUserSettings(opts?.reload)
if (!this.disabled) {
await installPackageThemes.call(null)
@@ -902,16 +953,17 @@ class PluginLocal extends EventEmitter<
} catch (e) {
this.logger.error('load', e, true)
- this.dispose().catch(null)
+ this.disposeRuntime().catch(null)
this._status = PluginLocalLoadStatus.ERROR
this._loadErr = e
} finally {
if (!this._loadErr) {
- if (this.disabled) {
- this._status = PluginLocalLoadStatus.UNLOADED
- } else {
- this._status = PluginLocalLoadStatus.LOADED
- }
+ this._transitionStatus(
+ this.disabled
+ ? PluginLocalLoadStatus.UNLOADED
+ : PluginLocalLoadStatus.LOADED,
+ [PluginLocalLoadStatus.LOADING]
+ )
}
}
}
@@ -922,7 +974,11 @@ class PluginLocal extends EventEmitter<
}
this._ctx.emit('beforereload', this)
- await this.unload()
+
+ if (this.loaded) {
+ await this.unload()
+ }
+
await this.load({ reload: true })
this._ctx.emit('reloaded', this)
}
@@ -935,8 +991,14 @@ class PluginLocal extends EventEmitter<
return
}
+ if (!unregister && !this.loaded) {
+ this._status = PluginLocalLoadStatus.UNLOADED
+ return
+ }
+
if (unregister) {
await this.unload()
+ await this.disposeRegistration()
if (this.isWebPlugin || this.isInstalledInLocalDotRoot) {
this._ctx.emit('unlink-plugin', this.id)
@@ -949,7 +1011,9 @@ class PluginLocal extends EventEmitter<
const eventBeforeUnload = { unregister }
if (this.loaded) {
- this._status = PluginLocalLoadStatus.UNLOADING
+ this._transitionStatus(PluginLocalLoadStatus.UNLOADING, [
+ PluginLocalLoadStatus.LOADED,
+ ])
try {
await this._caller?.callUserModel(
@@ -961,7 +1025,7 @@ class PluginLocal extends EventEmitter<
this.logger.error('beforeunload', e)
}
- await this.dispose()
+ await this.disposeRuntime()
}
this.emit('unloaded')
@@ -972,22 +1036,51 @@ class PluginLocal extends EventEmitter<
}
}
- private async dispose() {
- for (const fn of this._disposes) {
+ private async _runDisposers(disposers: Array<() => Promise>) {
+ for (const fn of disposers) {
try {
fn && (await fn())
} catch (e) {
console.error(this.debugTag, 'dispose Error', e)
}
}
+ }
+
+ private async disposeRuntime() {
+ await this._runDisposers(this._runtimeDisposes)
// clear
- this._disposes = []
+ this._runtimeDisposes = []
+ }
+
+ private async disposeRegistration() {
+ await this._runDisposers(this._registrationDisposes)
+
+ // clear
+ this._registrationDisposes = []
}
_dispose(fn: any) {
if (!fn) return
- this._disposes.push(fn)
+ this._runtimeDisposes.push(fn)
+ }
+
+ _disposeRegistration(fn: any) {
+ if (!fn) return
+ this._registrationDisposes.push(fn)
+ }
+
+ private _transitionStatus(
+ next: PluginLocalLoadStatus,
+ from?: PluginLocalLoadStatus[]
+ ) {
+ if (from && !from.includes(this._status)) {
+ throw new Error(
+ `Invalid plugin status transition: ${this._status} -> ${next}`
+ )
+ }
+
+ this._status = next
}
_onHostMounted(callback: () => void) {
@@ -1118,7 +1211,9 @@ class PluginLocal extends EventEmitter<
json.err = this.loadErr
json.usf = this.dotSettingsFile
json.iir = this.isInstalledInLocalDotRoot
- json.webMode = this.isWebPlugin ? (this.installedFromUserWebUrl ? 'user' : 'github') : false
+ json.webMode = this.isWebPlugin ? (this.installedFromUserWebUrl
+ ? 'user'
+ : 'github') : false
json.lsr = this._resolveResourceFullUrl('/')
if (settings === false) {
@@ -1278,12 +1373,11 @@ class LSPluginCore
// @ts-expect-error
window.__debugPluginsPerfInfo = debugPerfInfo
+ const readyIndicator = (this._readyIndicator = deferred())
+
try {
this._isRegistering = true
- const _userConfigRoot = this._options.dotConfigRoot
- const readyIndicator = (this._readyIndicator = deferred())
-
await this.loadUserPreferences()
let externals = new Set(this._userPreferences.externals)
@@ -1355,16 +1449,22 @@ class LSPluginCore
}
}
- pluginLocal.settings?.on('change', (a) => {
+ const onSettingsChange = (a) => {
this.emit('settings-changed', pluginLocal.id, a)
pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })
+ }
+
+ pluginLocal.settings?.on('change', onSettingsChange)
+ pluginLocal._disposeRegistration(() => {
+ pluginLocal.settings?.off('change', onSettingsChange)
})
this._registeredPlugins.set(pluginLocal.id, pluginLocal)
this.emit('registered', pluginLocal)
// external plugins
- if (!pluginLocal.isWebPlugin && !pluginLocal.isInstalledInLocalDotRoot) {
+ if (!pluginLocal.isWebPlugin &&
+ !pluginLocal.isInstalledInLocalDotRoot) {
externals.add(url)
}
}
@@ -1376,6 +1476,10 @@ class LSPluginCore
} catch (e) {
console.error(e)
} finally {
+ if (!readyIndicator.settled) {
+ readyIndicator.resolve('ready')
+ }
+
this._isRegistering = false
this.emit('ready', perfTable)
debugPerfInfo()
@@ -1432,18 +1536,26 @@ class LSPluginCore
async enable(plugin: PluginLocalIdentity) {
const p = this.ensurePlugin(plugin)
if (p.pending) return
+ if (!p.disabled && p.loaded) return
this.emit('beforeenable')
p.settings?.set('disabled', false)
+
+ await p.load()
+
this.emit('enabled', p.id)
}
async disable(plugin: PluginLocalIdentity) {
const p = this.ensurePlugin(plugin)
if (p.pending) return
+ if (p.disabled && !p.loaded) return
this.emit('beforedisable')
p.settings?.set('disabled', true)
+
+ await p.unload()
+
this.emit('disabled', p.id)
}
diff --git a/libs/src/LSPlugin.ts b/libs/src/LSPlugin.ts
index 6370426e7b..1ef537c2e6 100644
--- a/libs/src/LSPlugin.ts
+++ b/libs/src/LSPlugin.ts
@@ -487,6 +487,11 @@ export interface IAppProxy {
params?: Record,
query?: Record
) => void
+ getCurrentRoute: () => Promise<{
+ path: string
+ parameters: Record
+ template: string
+ }>
// templates
getTemplate: (name: string) => Promise
@@ -1129,10 +1134,15 @@ export interface ILSPluginUser extends EventEmitter {
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
*
+ * Patch the current plugin settings with the provided attributes.
+ *
* @param attrs
*/
updateSettings(attrs: Record): void
+ /**
+ * Called with full settings snapshots: `(nextSettings, previousSettings)`.
+ */
onSettingsChanged(cb: (a: T, b: T) => void): IUserOffHook
showSettingsUI(): void
diff --git a/libs/src/LSPlugin.user.ts b/libs/src/LSPlugin.user.ts
index 658d5b1e61..3833fb23b3 100644
--- a/libs/src/LSPlugin.user.ts
+++ b/libs/src/LSPlugin.user.ts
@@ -5,7 +5,7 @@ import {
PluginLogger,
safeSnakeCase,
safetyPathJoin, normalizeKeyStr,
-} from './helpers'
+} from './common'
import { LSPluginCaller } from './LSPlugin.caller'
import * as callableAPIs from './callable.apis'
import {
@@ -519,9 +519,15 @@ export class LSPluginUser
})
_caller.on('settings:changed', (payload) => {
- const b = Object.assign({}, this.settings)
- const a = Object.assign(this._baseInfo.settings, payload)
- this.emit('settings:changed', { ...a }, b)
+ const prevSettings = { ...(this.settings || {}) }
+ const nextSettings = { ...(payload || {}) }
+
+ this._baseInfo = {
+ ...this._baseInfo,
+ settings: nextSettings,
+ }
+
+ this.emit('settings:changed', nextSettings, prevSettings)
})
_caller.on('beforeunload', async (payload) => {
@@ -549,10 +555,14 @@ export class LSPluginUser
}
let baseInfo = await this._caller.connectToParent(model)
+ const hostSettings = baseInfo?.settings
this._connected = true
baseInfo = deepMerge(this._baseInfo, baseInfo)
+ if (hostSettings !== undefined) {
+ baseInfo.settings = hostSettings
+ }
this._baseInfo = baseInfo
if (baseInfo?.id) {
@@ -745,7 +755,7 @@ export class LSPluginUser
return function (this: any, ...args: any) {
if (origMethod) {
- if (args?.length !== 0) args.concat(nstag)
+ if (args?.length !== 0) args.push(nstag)
const ret = origMethod.apply(that, args)
if (ret !== PROXY_CONTINUE) return ret
}
diff --git a/libs/src/helpers.ts b/libs/src/common.ts
similarity index 96%
rename from libs/src/helpers.ts
rename to libs/src/common.ts
index 742dc761a3..077dc643f2 100644
--- a/libs/src/helpers.ts
+++ b/libs/src/common.ts
@@ -21,7 +21,7 @@ export const PROTOCOL_FILE = 'file://'
export const PROTOCOL_LSP = 'lsp://'
export const URL_LSP = PROTOCOL_LSP + 'logseq.io/'
-let _appPathRoot
+let _appPathRoot: string
// TODO: snakeCase of lodash is incompatible with `snake-case`
export const safeSnakeCase = snakeCase
@@ -169,10 +169,6 @@ export function safetyPathNormalize(basePath: string) {
return basePath
}
-/**
- * @param timeout milliseconds
- * @param tag string
- */
export function deferred(timeout?: number, tag?: string) {
let resolve: any, reject: any
let settled = false
@@ -242,25 +238,6 @@ export function invokeHostExportedApi(method: string, ...args: Array) {
return typeof fn !== 'function' ? fn : fn.apply(this, args)
}
-export function setupIframeSandbox(
- props: Record,
- target: HTMLElement
-) {
- const iframe = document.createElement('iframe')
-
- iframe.classList.add('lsp-iframe-sandbox')
-
- Object.entries(props).forEach(([k, v]) => {
- iframe.setAttribute(k, v)
- })
-
- target.appendChild(iframe)
-
- return async () => {
- target.removeChild(iframe)
- }
-}
-
export function setupInjectedStyle(
style: StyleString,
attrs: Record
diff --git a/libs/src/modules/LSPlugin.Experiments.ts b/libs/src/modules/LSPlugin.Experiments.ts
index 6ec50e5d4e..b032107643 100644
--- a/libs/src/modules/LSPlugin.Experiments.ts
+++ b/libs/src/modules/LSPlugin.Experiments.ts
@@ -1,6 +1,6 @@
import { LSPluginUser } from '../LSPlugin.user'
import { PluginLocal } from '../LSPlugin.core'
-import { safeSnakeCase } from '../helpers'
+import { safeSnakeCase } from '../common'
/**
* WARN: These are some experience features and might be adjusted at any time.
@@ -95,6 +95,37 @@ export class LSPluginExperiments {
)
}
+ registerHostedRenderer(
+ key: string,
+ opts: {
+ title?: string,
+ subs?: Array
+ type?: string,
+ render: (props: {}) => any
+ }
+ ) {
+ return this.invokeExperMethod(
+ 'registerHostedRenderer',
+ this.ctx.baseInfo.id,
+ key,
+ opts
+ )
+ }
+
+ registerSidebarRenderer(
+ key: string,
+ opts: {
+ title?: string,
+ subs?: Array
+ render: (props: {}) => any,
+ [k: string]: any
+ }
+ ) {
+ key = `_sidebar.${key}`
+ opts.type = 'sidebar'
+ return this.registerHostedRenderer(key, opts)
+ }
+
registerRouteRenderer(
key: string,
opts: {
diff --git a/libs/src/postmate/index.ts b/libs/src/postmate/index.ts
index b6b5ad9bb6..3fb69b7616 100644
--- a/libs/src/postmate/index.ts
+++ b/libs/src/postmate/index.ts
@@ -1,40 +1,14 @@
// Fork from https://github.com/dollarshaveclub/postmate
-/**
- * The type of messages our frames our sending
- * @type {String}
- */
export const messageType = 'application/x-postmate-v1+json'
-
-/**
- * The maximum number of attempts to send a handshake request to the parent
- * @type {Number}
- */
+export const defaultRequestTimeout = 10_000
export const maxHandshakeRequests = 5
-/**
- * A unique message ID that is used to ensure responses are sent to the correct requests
- * @type {Number}
- */
let _messageId = 0
-/**
- * Increments and returns a message ID
- * @return {Number} A unique ID for a message
- */
-export const generateNewMessageId = () => ++_messageId
-
-/**
- * Postmate logging function that enables/disables via config
- */
-export const log = (...args) => (Postmate.debug ? console.log(...args) : null)
-
-/**
- * Takes a URL and returns the origin
- * @param {String} url The full URL being requested
- * @return {String} The URLs origin
- */
-export const resolveOrigin = (url) => {
+const generateNewMessageId = () => ++_messageId
+const log = (...args: any) => (Postmate.debug ? console.log(...args) : null)
+const resolveOrigin = (url: string) => {
const a = document.createElement('a')
a.href = url
const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol
@@ -55,13 +29,7 @@ const messageTypes = {
request: 1,
}
-/**
- * Ensures that a message is safe to interpret
- * @param {Object} message The postmate message being sent
- * @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
- * @return {Boolean}
- */
-export const sanitize = (message, allowedOrigin) => {
+export const sanitize = (message: any, allowedOrigin: any) => {
if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin)
return false
if (!message.data) return false
@@ -72,14 +40,8 @@ export const sanitize = (message, allowedOrigin) => {
return true
}
-/**
- * Takes a model, and searches for a value by the property
- * @param {Object} model The dictionary to search against
- * @param {String} property A path within a dictionary (i.e. 'window.location.href')
- * passed to functions in the child model
- * @return {Promise}
- */
-export const resolveValue = (model, property, args) => {
+export const resolveValue = (model: any, property: string, args: Array) => {
+ // args arguments passed from parent to child function
const unwrappedContext =
typeof model[property] === 'function'
? model[property].apply(null, args)
@@ -98,12 +60,44 @@ export class ParentAPI {
public events = {}
public childOrigin: string
public listener: (e: any) => void
+ private readonly messagePort?: MessagePort
+
+ private addTransportListener(handler: (e: any) => void) {
+ if (this.messagePort) {
+ console.debug('[DEBUG] Using MessagePort for communication:', this.frame.src)
+ // MessagePort delivers MessageEvent too, but without origin/source.
+ this.messagePort.addEventListener('message', handler as any)
+ // Some browsers require start() when using addEventListener.
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ ;(this.messagePort as any).start?.()
+ } else {
+ console.debug('[DEBUG] Using postMessage for communication:', this.frame.src)
+ this.parent.addEventListener('message', handler, false)
+ }
+ }
+
+ private removeTransportListener(handler: (e: any) => void) {
+ if (this.messagePort) {
+ this.messagePort.removeEventListener('message', handler as any)
+ } else {
+ this.parent.removeEventListener('message', handler, false)
+ }
+ }
+
+ private postToChild(payload: any) {
+ if (this.messagePort) {
+ this.messagePort.postMessage(payload)
+ return
+ }
+ this.child.postMessage(payload, this.childOrigin)
+ }
constructor(info: Postmate) {
this.parent = info.parent
this.frame = info.frame
this.child = info.child
this.childOrigin = info.childOrigin
+ this.messagePort = info.messagePort
if (process.env.NODE_ENV !== 'production') {
log('Parent: Registering API')
@@ -111,7 +105,15 @@ export class ParentAPI {
}
this.listener = (e) => {
- if (!sanitize(e, this.childOrigin)) return false
+ // Port messages don't have origin/source, so we only enforce postmate/type.
+ if (this.messagePort) {
+ if (!e?.data) return false
+ if (typeof e.data === 'object' && !('postmate' in e.data)) return false
+ if (e.data.type !== messageType) return false
+ if (!messageTypes[e.data.postmate]) return false
+ } else {
+ if (!sanitize(e, this.childOrigin)) return false
+ }
/**
* the assignments below ensures that e, data, and value are all defined
@@ -123,26 +125,35 @@ export class ParentAPI {
log(`Parent: Received event emission: ${name}`)
}
if (name in this.events) {
- this.events[name].forEach((callback) => {
+ this.events[name].forEach((callback: Function) => {
callback.call(this, data)
})
}
}
}
- this.parent.addEventListener('message', this.listener, false)
+ // NOTE: older SDKs rely on window.postMessage only, so we must
+ // initially add listener on window (messagePort is not set yet at construction time)
+ this.addTransportListener(this.listener)
if (process.env.NODE_ENV !== 'production') {
log('Parent: Awaiting event emissions from Child')
}
}
- get(property, ...args) {
+ get(property: string, ...args: any) {
return new Promise((resolve, reject) => {
// Extract data from response and kill listeners
const uid = generateNewMessageId()
+ const timeoutMs =
+ typeof (Postmate as any).requestTimeout === 'number'
+ ? (Postmate as any).requestTimeout
+ : defaultRequestTimeout
+
+ let timer: any
const transact = (e) => {
- if (e.data.uid === uid && e.data.postmate === 'reply') {
- this.parent.removeEventListener('message', transact, false)
+ if (e?.data?.uid === uid && e.data.postmate === 'reply') {
+ this.removeTransportListener(transact)
+ if (timer) clearTimeout(timer)
if (e.data.error) {
reject(e.data.error)
} else {
@@ -152,36 +163,37 @@ export class ParentAPI {
}
// Prepare for response from Child...
- this.parent.addEventListener('message', transact, false)
+ this.addTransportListener(transact)
+
+ if (timeoutMs > 0) {
+ timer = setTimeout(() => {
+ this.removeTransportListener(transact)
+ reject(new Error(`Postmate: request timeout (${timeoutMs}ms)`))
+ }, timeoutMs)
+ }
// Then ask child for information
- this.child.postMessage(
- {
- postmate: 'request',
- type: messageType,
- property,
- args,
- uid,
- },
- this.childOrigin
- )
+ this.postToChild({
+ postmate: 'request',
+ type: messageType,
+ property,
+ args,
+ uid,
+ })
})
}
- call(property, data) {
+ call(property: string, data: any) {
// Send information to the child
- this.child.postMessage(
- {
- postmate: 'call',
- type: messageType,
- property,
- data,
- },
- this.childOrigin
- )
+ this.postToChild({
+ postmate: 'call',
+ type: messageType,
+ property,
+ data,
+ })
}
- on(eventName, callback) {
+ on(eventName: string, callback: Function) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
@@ -192,37 +204,69 @@ export class ParentAPI {
if (process.env.NODE_ENV !== 'production') {
log('Parent: Destroying Postmate instance')
}
- window.removeEventListener('message', this.listener, false)
+ this.removeTransportListener(this.listener)
+ try {
+ this.messagePort?.close()
+ } catch (e) {
+ // ignore
+ }
this.frame.parentNode.removeChild(this.frame)
}
}
-/**
- * Composes an API to be used by the child
- * @param {Object} info Information on the consumer
- */
+// Composes an API to be used by the child
export class ChildAPI {
- private model: any
+ private readonly model: any
private parent: Window
- private parentOrigin: string
+ private readonly parentOrigin: string
private child: Window
+ private messagePort?: MessagePort
+ private readonly listener: (e: any) => void
+
+ private addTransportListener(handler: (e: any) => void) {
+ if (this.messagePort) {
+ this.messagePort.addEventListener('message', handler as any)
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ ;(this.messagePort as any).start?.()
+ } else {
+ this.child.addEventListener('message', handler, false)
+ }
+ }
+
+ private postToParent(payload: any, fallbackEvent?: MessageEvent) {
+ if (this.messagePort) {
+ this.messagePort.postMessage(payload)
+ return
+ }
+ // reply uses the event source/origin, others use stored parentOrigin.
+ if (fallbackEvent?.source) {
+ ;(fallbackEvent.source as WindowProxy).postMessage(payload, fallbackEvent.origin)
+ } else {
+ this.parent.postMessage(payload, this.parentOrigin)
+ }
+ }
constructor(info: Model) {
this.model = info.model
this.parent = info.parent
this.parentOrigin = info.parentOrigin
this.child = info.child
+ this.messagePort = info.messagePort
if (process.env.NODE_ENV !== 'production') {
log('Child: Registering API')
log('Child: Awaiting messages...')
}
- this.child.addEventListener('message', (e) => {
- if (!sanitize(e, this.parentOrigin)) return
-
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Received request', e.data)
+ this.listener = (e) => {
+ // Port messages don't have origin/source, so we only enforce postmate/type.
+ if (this.messagePort) {
+ if (!e?.data) return
+ if (typeof e.data === 'object' && !('postmate' in e.data)) return
+ if (e.data.type !== messageType) return
+ if (!messageTypes[e.data.postmate]) return
+ } else {
+ if (!sanitize(e, this.parentOrigin)) return
}
const { property, uid, data, args } = e.data
@@ -238,47 +282,79 @@ export class ChildAPI {
}
// Reply to Parent
- resolveValue(this.model, property, args).then((value) => {
- ;(e.source as WindowProxy).postMessage(
- {
- property,
- postmate: 'reply',
- type: messageType,
- uid,
- value,
- },
- e.origin
- )
- }).catch((error) => {
- ;(e.source as WindowProxy).postMessage(
- {
- property,
- postmate: 'reply',
- type: messageType,
- uid,
- error,
- },
- e.origin
- )
- })
- })
+ resolveValue(this.model, property, args)
+ .then((value) => {
+ this.postToParent(
+ {
+ property,
+ postmate: 'reply',
+ type: messageType,
+ uid,
+ value,
+ },
+ e
+ )
+ })
+ .catch((error) => {
+ this.postToParent(
+ {
+ property,
+ postmate: 'reply',
+ type: messageType,
+ uid,
+ error,
+ },
+ e
+ )
+ })
+ }
+
+ // Initially add listener on window (messagePort is not set yet at construction time)
+ this.addTransportListener(this.listener)
+
+ // Listen for channel ready event to switch transport from window to MessagePort
+ if (!this.messagePort) {
+ const channelReadyHandler = (event: CustomEvent) => {
+ const port = event.detail?.port
+ if (port) {
+ if (process.env.NODE_ENV !== 'production') {
+ log('Child: Switching from window.postMessage to MessagePort')
+ }
+
+ // Step 1: Remove listener from window to avoid duplicate message handling
+ this.child.removeEventListener('message', this.listener, false)
+
+ // Step 2: Update messagePort reference (this affects this.listener validation logic)
+ this.messagePort = port
+
+ // Step 3: Add the same listener to MessagePort (now all communication goes through port)
+ this.messagePort.addEventListener('message', this.listener as any)
+ ;(this.messagePort as any).start?.()
+
+ if (process.env.NODE_ENV !== 'production') {
+ log('Child: Successfully switched to MessagePort. All future messages will use MessageChannel.')
+ }
+
+ // Step 4: Clean up the channel-ready event listener
+ this.child.removeEventListener('postmate:channel-ready', channelReadyHandler as any)
+ }
+ }
+ this.child.addEventListener('postmate:channel-ready', channelReadyHandler as any)
+ }
}
- emit(name, data) {
+ emit(name: string, data: any) {
if (process.env.NODE_ENV !== 'production') {
log(`Child: Emitting Event "${name}"`, data)
}
- this.parent.postMessage(
- {
- postmate: 'emit',
- type: messageType,
- value: {
- name,
- data,
- },
+ this.postToParent({
+ postmate: 'emit',
+ type: messageType,
+ value: {
+ name,
+ data,
},
- this.parentOrigin
- )
+ })
}
}
@@ -290,6 +366,11 @@ export type PostMateOptions = {
name?: string
model?: any,
allow?: string
+ /**
+ * Prefer using MessageChannel/MessagePort after handshake.
+ * Defaults to false to keep backward-compatible behavior with older SDKs.
+ */
+ enableMessageChannel?: boolean
}
/**
@@ -297,6 +378,7 @@ export type PostMateOptions = {
*/
export class Postmate {
static debug = false // eslint-disable-line no-undef
+ static requestTimeout: number = defaultRequestTimeout
public container?: HTMLElement
public parent: Window
public frame: HTMLIFrameElement
@@ -306,9 +388,10 @@ export class Postmate {
public model: any
static Model: any
- /**
- * @param opts
- */
+ // Preferred transport after handshake.
+ public messagePort?: MessagePort
+ private readonly enableMessageChannel: boolean
+
constructor(opts: PostMateOptions) {
this.container = opts.container
this.url = opts.url
@@ -324,19 +407,22 @@ export class Postmate {
this.container.appendChild(this.frame)
this.child = this.frame.contentWindow
this.model = opts.model || {}
+ this.enableMessageChannel = !!opts.enableMessageChannel
}
- /**
- * Begins the handshake strategy
- * @param {String} url The URL to send a handshake request to
- * @return {Promise} Promise that resolves when the handshake is complete
- */
sendHandshake(url?: string) {
url = url || this.url
const childOrigin = resolveOrigin(url)
let attempt = 0
- let responseInterval
+ let responseInterval: any
return new Promise((resolve, reject) => {
+ const runtimeSupportsMessageChannel =
+ typeof MessageChannel !== 'undefined' &&
+ typeof (MessageChannel as any) === 'function'
+
+ const shouldUseMessageChannel =
+ this.enableMessageChannel && runtimeSupportsMessageChannel
+
const reply = (e: any) => {
if (!sanitize(e, childOrigin)) return false
if (e.data.postmate === 'handshake-reply') {
@@ -346,6 +432,40 @@ export class Postmate {
}
this.parent.removeEventListener('message', reply, false)
this.childOrigin = e.origin
+
+ // After successful handshake, establish MessageChannel if enabled and child supports it
+ if (shouldUseMessageChannel) {
+ // Check if child returned a port (child initiated the channel)
+ if (e?.ports?.length) {
+ const returnedPort = e.ports[0]
+ if (returnedPort) {
+ this.messagePort = returnedPort
+ ;(this.messagePort as any).start?.()
+ if (process.env.NODE_ENV !== 'production') {
+ log('Parent: Using MessageChannel returned by child')
+ }
+ }
+ } else if (e.data.acceptsMessageChannel) {
+ // Child signals it accepts MessageChannel, so parent creates and transfers port
+ const channel = new MessageChannel()
+ this.messagePort = channel.port1
+ ;(this.messagePort as any).start?.()
+
+ // Send port2 to child in a follow-up message
+ this.child.postMessage(
+ {
+ postmate: 'setup-channel',
+ type: messageType,
+ },
+ childOrigin,
+ [channel.port2]
+ )
+ if (process.env.NODE_ENV !== 'production') {
+ log('Parent: Sent MessageChannel port to child')
+ }
+ }
+ }
+
if (process.env.NODE_ENV !== 'production') {
log('Parent: Saving Child origin', this.childOrigin)
}
@@ -367,14 +487,17 @@ export class Postmate {
if (process.env.NODE_ENV !== 'production') {
log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin })
}
- this.child.postMessage(
- {
- postmate: 'handshake',
- type: messageType,
- model: this.model,
- },
- childOrigin
- )
+
+ // Always use window.postMessage for handshake to maintain backward compatibility
+ const payload: any = {
+ postmate: 'handshake',
+ type: messageType,
+ model: this.model,
+ // Signal to child that parent supports MessageChannel
+ enableMessageChannel: shouldUseMessageChannel ? 1 : 0,
+ }
+
+ this.child.postMessage(payload, childOrigin)
if (attempt === maxHandshakeRequests) {
clearInterval(responseInterval)
@@ -399,6 +522,11 @@ export class Postmate {
if (process.env.NODE_ENV !== 'production') {
log('Postmate: Destroying Postmate instance')
}
+ try {
+ this.messagePort?.close()
+ } catch (e) {
+ // ignore
+ }
this.frame.parentNode.removeChild(this.frame)
}
}
@@ -411,22 +539,17 @@ export class Model {
public model: any
public parent: Window
public parentOrigin: string
+ public messagePort?: MessagePort
+ private enableMessageChannel: boolean
- /**
- * Initializes the child, model, parent, and responds to the Parents handshake
- * @param {Object} model Hash of values, functions, or promises
- * @return {Promise} The Promise that resolves when the handshake has been received
- */
- constructor(model) {
+ constructor(model: any) {
this.child = window
this.model = model
this.parent = this.child.parent
+ // Child side is controlled by what parent sends in handshake; default false.
+ this.enableMessageChannel = false
}
- /**
- * Responds to a handshake initiated by the Parent
- * @return {Promise} Resolves an object that exposes an API for the Child
- */
sendHandshakeReply() {
return new Promise((resolve, reject) => {
const shake = (e: MessageEvent) => {
@@ -438,17 +561,30 @@ export class Model {
log('Child: Received handshake from Parent')
}
this.child.removeEventListener('message', shake, false)
+
+ // Check if parent supports MessageChannel (but don't use it yet during handshake)
+ this.enableMessageChannel = !!e.data?.enableMessageChannel
+ this.parentOrigin = e.origin
+
if (process.env.NODE_ENV !== 'production') {
log('Child: Sending handshake reply to Parent')
}
+
+ const runtimeSupportsMessageChannel =
+ typeof MessageChannel !== 'undefined' &&
+ typeof (MessageChannel as any) === 'function'
+
+ // Reply using window.postMessage for backward compatibility
+ // Signal whether child accepts MessageChannel for future communication
;(e.source as WindowProxy).postMessage(
{
postmate: 'handshake-reply',
type: messageType,
+ // Tell parent if child can accept MessageChannel
+ acceptsMessageChannel: this.enableMessageChannel && runtimeSupportsMessageChannel ? 1 : 0,
},
e.origin
)
- this.parentOrigin = e.origin
// Extend model with the one provided by the parent
const defaults = e.data.model
@@ -461,6 +597,34 @@ export class Model {
}
}
+ // Set up listener for MessageChannel port if both sides support it
+ if (this.enableMessageChannel && runtimeSupportsMessageChannel) {
+ const setupChannel = (setupEvent: MessageEvent) => {
+ if (
+ setupEvent.data?.postmate === 'setup-channel' &&
+ setupEvent.data?.type === messageType &&
+ setupEvent.origin === this.parentOrigin
+ ) {
+ const transferredPort = setupEvent?.ports?.[0]
+ if (transferredPort) {
+ this.messagePort = transferredPort
+ ;(this.messagePort as any).start?.()
+ if (process.env.NODE_ENV !== 'production') {
+ log('Child: Received and activated MessageChannel port from parent')
+ }
+
+ // Important: Notify the ChildAPI instance (created below) to switch transport
+ // We'll emit a custom event that ChildAPI can listen to
+ this.child.dispatchEvent(new CustomEvent('postmate:channel-ready', {
+ detail: { port: this.messagePort }
+ }))
+ }
+ this.child.removeEventListener('message', setupChannel, false)
+ }
+ }
+ this.child.addEventListener('message', setupChannel, false)
+ }
+
if (process.env.NODE_ENV !== 'production') {
log('Child: Saving Parent origin', this.parentOrigin)
}
diff --git a/resources/js/lsplugin.core.js b/resources/js/lsplugin.core.js
index df7eb97e1a..dc23e36646 100644
--- a/resources/js/lsplugin.core.js
+++ b/resources/js/lsplugin.core.js
@@ -1,2 +1,2 @@
/*! For license information please see lsplugin.core.js.LICENSE.txt */
-!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LSPlugin=t():e.LSPlugin=t()}(self,()=>(()=>{var e={833:(e,t,n)=>{var r=n(606);t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let r=0,i=0;t[0].replace(/%[a-zA-Z%]/g,e=>{"%%"!==e&&(r++,"%c"===e&&(i=r))}),t.splice(i,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")}catch(e){}return!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG),e},t.useColors=function(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(736)(t);const{formatters:i}=e.exports;i.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},736:(e,t,n)=>{e.exports=function(e){function t(e){let n,i,o,s=null;function a(...e){if(!a.enabled)return;const r=a,i=Number(new Date),o=i-(n||i);r.diff=o,r.prev=n,r.curr=i,n=i,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let s=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,(n,i)=>{if("%%"===n)return"%";s++;const o=t.formatters[i];if("function"==typeof o){const t=e[s];n=o.call(r,t),e.splice(s,1),s--}return n}),t.formatArgs.call(r,e),(r.log||t.log).apply(r,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=r,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==s?s:(i!==t.namespaces&&(i=t.namespaces,o=t.enabled(e)),o),set:e=>{s=e}}),"function"==typeof t.init&&t.init(a),a}function r(e,n){const r=t(this.namespace+(void 0===n?":":n)+e);return r.log=this.log,r}function i(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return t.debug=t,t.default=t,t.coerce=function(e){return e instanceof Error?e.stack||e.message:e},t.disable=function(){const e=[...t.names.map(i),...t.skips.map(i).map(e=>"-"+e)].join(",");return t.enable(""),e},t.enable=function(e){let n;t.save(e),t.namespaces=e,t.names=[],t.skips=[];const r=("string"==typeof e?e:"").split(/[\s,]+/),i=r.length;for(n=0;n{t[n]=e[n]}),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)},n="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?a((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function i(e,t,n){return e.concat(t).map(function(e){return r(e,n)})}function o(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter(function(t){return Object.propertyIsEnumerable.call(e,t)}):[]}(e))}function s(e,t){try{return t in e}catch(e){return!1}}function a(e,n,l){(l=l||{}).arrayMerge=l.arrayMerge||i,l.isMergeableObject=l.isMergeableObject||t,l.cloneUnlessOtherwiseSpecified=r;var c=Array.isArray(n);return c===Array.isArray(e)?c?l.arrayMerge(e,n,l):function(e,t,n){var i={};return n.isMergeableObject(e)&&o(e).forEach(function(t){i[t]=r(e[t],n)}),o(t).forEach(function(o){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,o)||(s(e,o)&&n.isMergeableObject(t[o])?i[o]=function(e,t){if(!t.customMerge)return a;var n=t.customMerge(e);return"function"==typeof n?n:a}(o,n)(e[o],t[o],n):i[o]=r(t[o],n))}),i}(e,n,l):r(n,l)}a.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce(function(e,n){return a(e,n,t)},{})};var l=a;e.exports=l},228:e=>{"use strict";var t=Object.prototype.hasOwnProperty,n="~";function r(){}function i(e,t,n){this.fn=e,this.context=t,this.once=n||!1}function o(e,t,r,o,s){if("function"!=typeof r)throw new TypeError("The listener must be a function");var a=new i(r,o||e,s),l=n?n+t:t;return e._events[l]?e._events[l].fn?e._events[l]=[e._events[l],a]:e._events[l].push(a):(e._events[l]=a,e._eventsCount++),e}function s(e,t){0===--e._eventsCount?e._events=new r:delete e._events[t]}function a(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),a.prototype.eventNames=function(){var e,r,i=[];if(0===this._eventsCount)return i;for(r in e=this._events)t.call(e,r)&&i.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(e)):i},a.prototype.listeners=function(e){var t=n?n+e:e,r=this._events[t];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,o=r.length,s=new Array(o);i{"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},585:e=>{var t=1e3,n=60*t,r=60*n,i=24*r,o=7*i;function s(e,t,n,r){var i=t>=1.5*n;return Math.round(e/n)+" "+r+(i?"s":"")}e.exports=function(e,a){a=a||{};var l,c,u=typeof e;if("string"===u&&e.length>0)return function(e){if(!((e=String(e)).length>100)){var s=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(s){var a=parseFloat(s[1]);switch((s[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return 315576e5*a;case"weeks":case"week":case"w":return a*o;case"days":case"day":case"d":return a*i;case"hours":case"hour":case"hrs":case"hr":case"h":return a*r;case"minutes":case"minute":case"mins":case"min":case"m":return a*n;case"seconds":case"second":case"secs":case"sec":case"s":return a*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return a;default:return}}}}(e);if("number"===u&&isFinite(e))return a.long?(l=e,(c=Math.abs(l))>=i?s(l,c,i,"day"):c>=r?s(l,c,r,"hour"):c>=n?s(l,c,n,"minute"):c>=t?s(l,c,t,"second"):l+" ms"):function(e){var o=Math.abs(e);return o>=i?Math.round(e/i)+"d":o>=r?Math.round(e/r)+"h":o>=n?Math.round(e/n)+"m":o>=t?Math.round(e/t)+"s":e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},627:(e,t,n)=>{"use strict";var r=n(606),i="win32"===r.platform,o=n(537);function s(e,t){for(var n=[],r=0;r=0&&!e[r];r--);return 0===n&&r===t?e:n>r?[]:e.slice(n,r+1)}var l=/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/,c=/^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/,u={};function h(e){var t=l.exec(e),n=(t[1]||"")+(t[2]||""),r=t[3]||"",i=c.exec(r);return[n,i[1],i[2],i[3]]}function d(e){var t=l.exec(e),n=t[1]||"",r=!!n&&":"!==n[1];return{device:n,isUnc:r,isAbsolute:r||!!t[2],tail:t[3]}}function p(e){return"\\\\"+e.replace(/^[\\\/]+/,"").replace(/[\\\/]+/g,"\\")}u.resolve=function(){for(var e="",t="",n=!1,i=arguments.length-1;i>=-1;i--){var a;if(i>=0?a=arguments[i]:e?(a=r.env["="+e])&&a.substr(0,3).toLowerCase()===e.toLowerCase()+"\\"||(a=e+"\\"):a=r.cwd(),!o.isString(a))throw new TypeError("Arguments to path.resolve must be strings");if(a){var l=d(a),c=l.device,u=l.isUnc,h=l.isAbsolute,f=l.tail;if((!c||!e||c.toLowerCase()===e.toLowerCase())&&(e||(e=c),n||(t=f+"\\"+t,n=h),e&&n))break}}return u&&(e=p(e)),e+(n?"\\":"")+(t=s(t.split(/[\\\/]+/),!n).join("\\"))||"."},u.normalize=function(e){var t=d(e),n=t.device,r=t.isUnc,i=t.isAbsolute,o=t.tail,a=/[\\\/]$/.test(o);return(o=s(o.split(/[\\\/]+/),!i).join("\\"))||i||(o="."),o&&a&&(o+="\\"),r&&(n=p(n)),n+(i?"\\":"")+o},u.isAbsolute=function(e){return d(e).isAbsolute},u.join=function(){for(var e=[],t=0;t=-1&&!t;n--){var i=n>=0?arguments[n]:r.cwd();if(!o.isString(i))throw new TypeError("Arguments to path.resolve must be strings");i&&(e=i+"/"+e,t="/"===i[0])}return(t?"/":"")+(e=s(e.split("/"),!t).join("/"))||"."},g.normalize=function(e){var t=g.isAbsolute(e),n=e&&"/"===e[e.length-1];return(e=s(e.split("/"),!t).join("/"))||t||(e="."),e&&n&&(e+="/"),(t?"/":"")+e},g.isAbsolute=function(e){return"/"===e.charAt(0)},g.join=function(){for(var e="",t=0;t{var t,n,r=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function s(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:i}catch(e){t=i}try{n="function"==typeof clearTimeout?clearTimeout:o}catch(e){n=o}}();var a,l=[],c=!1,u=-1;function h(){c&&a&&(c=!1,a.length?l=a.concat(l):u=-1,l.length&&d())}function d(){if(!c){var e=s(h);c=!0;for(var t=l.length;t;){for(a=l,l=[];++u1)for(var n=1;n{e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},537:(e,t,n)=>{var r=n(606),i=/%[sdj%]/g;t.format=function(e){if(!y(e)){for(var t=[],n=0;n=o)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),l=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),f(n)?r.showHidden=n:n&&t._extend(r,n),_(r.showHidden)&&(r.showHidden=!1),_(r.depth)&&(r.depth=2),_(r.colors)&&(r.colors=!1),_(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=l),u(r,e,r.depth)}function l(e,t){var n=a.styles[t];return n?"["+a.colors[n][0]+"m"+e+"["+a.colors[n][1]+"m":e}function c(e,t){return e}function u(e,n,r){if(e.customInspect&&n&&S(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var i=n.inspect(r,e);return y(i)||(i=u(e,i,r)),i}var o=function(e,t){if(_(t))return e.stylize("undefined","undefined");if(y(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}return m(t)?e.stylize(""+t,"number"):f(t)?e.stylize(""+t,"boolean"):g(t)?e.stylize("null","null"):void 0}(e,n);if(o)return o;var s=Object.keys(n),a=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(s);if(e.showHidden&&(s=Object.getOwnPropertyNames(n)),x(n)&&(s.indexOf("message")>=0||s.indexOf("description")>=0))return h(n);if(0===s.length){if(S(n)){var l=n.name?": "+n.name:"";return e.stylize("[Function"+l+"]","special")}if(b(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(w(n))return e.stylize(Date.prototype.toString.call(n),"date");if(x(n))return h(n)}var c,v="",E=!1,C=["{","}"];return p(n)&&(E=!0,C=["[","]"]),S(n)&&(v=" [Function"+(n.name?": "+n.name:"")+"]"),b(n)&&(v=" "+RegExp.prototype.toString.call(n)),w(n)&&(v=" "+Date.prototype.toUTCString.call(n)),x(n)&&(v=" "+h(n)),0!==s.length||E&&0!=n.length?r<0?b(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),c=E?function(e,t,n,r,i){for(var o=[],s=0,a=t.length;s60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}(c,v,C)):C[0]+v+C[1]}function h(e){return"["+Error.prototype.toString.call(e)+"]"}function d(e,t,n,r,i,o){var s,a,l;if((l=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]}).get?a=l.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):l.set&&(a=e.stylize("[Setter]","special")),O(r,i)||(s="["+i+"]"),a||(e.seen.indexOf(l.value)<0?(a=g(n)?u(e,l.value,null):u(e,l.value,n-1)).indexOf("\n")>-1&&(a=o?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),_(s)){if(o&&i.match(/^\d+$/))return a;(s=JSON.stringify(""+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(s=s.substr(1,s.length-2),s=e.stylize(s,"name")):(s=s.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),s=e.stylize(s,"string"))}return s+": "+a}function p(e){return Array.isArray(e)}function f(e){return"boolean"==typeof e}function g(e){return null===e}function m(e){return"number"==typeof e}function y(e){return"string"==typeof e}function _(e){return void 0===e}function b(e){return v(e)&&"[object RegExp]"===E(e)}function v(e){return"object"==typeof e&&null!==e}function w(e){return v(e)&&"[object Date]"===E(e)}function x(e){return v(e)&&("[object Error]"===E(e)||e instanceof Error)}function S(e){return"function"==typeof e}function E(e){return Object.prototype.toString.call(e)}function C(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(e){if(_(o)&&(o=r.env.NODE_DEBUG||""),e=e.toUpperCase(),!s[e])if(new RegExp("\\b"+e+"\\b","i").test(o)){var n=r.pid;s[e]=function(){var r=t.format.apply(t,arguments);console.error("%s %d: %s",e,n,r)}}else s[e]=function(){};return s[e]},t.inspect=a,a.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},a.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=f,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=m,t.isString=y,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=_,t.isRegExp=b,t.isObject=v,t.isDate=w,t.isError=x,t.isFunction=S,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(135);var A=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function O(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(n=[C((e=new Date).getHours()),C(e.getMinutes()),C(e.getSeconds())].join(":"),[e.getDate(),A[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(698),t._extend=function(e,t){if(!t||!v(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e}}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};return(()=>{"use strict";n.r(r),n.d(r,{PluginLocal:()=>_i,pluginHelpers:()=>t,setupPluginCore:()=>vi});var e={};n.r(e),n.d(e,{setSDKMetadata:()=>fe});var t={};n.r(t),n.d(t,{IS_DEV:()=>me,PROTOCOL_FILE:()=>ye,PROTOCOL_LSP:()=>_e,PluginLogger:()=>Ae,URL_LSP:()=>be,cleanInjectedScripts:()=>$e,cleanInjectedUI:()=>Ue,deepMerge:()=>Ce,deferred:()=>Le,genID:()=>Te,getAppPathRoot:()=>xe,getSDKPathRoot:()=>Se,injectTheme:()=>He,invokeHostExportedApi:()=>Re,isObject:()=>Ee,isValidUUID:()=>Oe,mergeSettingsWithSchema:()=>We,normalizeKeyStr:()=>qe,path:()=>ge,safeSnakeCase:()=>we,safetyPathJoin:()=>je,safetyPathNormalize:()=>Pe,setupIframeSandbox:()=>Me,setupInjectedStyle:()=>De,setupInjectedUI:()=>Fe,transformableEvent:()=>ze,ucFirst:()=>ke,withFileProtocol:()=>Ie});var i=n(228),o=n.n(i),s=n(627);const{entries:a,setPrototypeOf:l,isFrozen:c,getPrototypeOf:u,getOwnPropertyDescriptor:h}=Object;let{freeze:d,seal:p,create:f}=Object,{apply:g,construct:m}="undefined"!=typeof Reflect&&Reflect;d||(d=function(e){return e}),p||(p=function(e){return e}),g||(g=function(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),i=2;i1?t-1:0),r=1;r1?n-1:0),i=1;i2&&void 0!==arguments[2]?arguments[2]:x;l&&l(e,null);let r=t.length;for(;r--;){let i=t[r];if("string"==typeof i){const e=n(i);e!==i&&(c(t)||(t[r]=e),i=e)}e[i]=!0}return e}function R(e){for(let t=0;t/gm),V=p(/\$\{[\w\W]*/gm),Z=p(/^data-[\-\w.\u00B7-\uFFFF]+$/),X=p(/^aria-[\-\w]+$/),Q=p(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ee=p(/^(?:\w+script|data):/i),te=p(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ne=p(/^html$/i),re=p(/^[a-z][.\w]*(-[.\w]+)+$/i);var ie=Object.freeze({__proto__:null,ARIA_ATTR:X,ATTR_WHITESPACE:te,CUSTOM_ELEMENT:re,DATA_ATTR:Z,DOCTYPE_NAME:ne,ERB_EXPR:K,IS_ALLOWED_URI:Q,IS_SCRIPT_OR_DATA:ee,MUSTACHE_EXPR:Y,TMPLIT_EXPR:V});const oe=function(){return"undefined"==typeof window?null:window};var se=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const n=t=>e(t);if(n.version="3.3.1",n.removed=[],!t||!t.document||9!==t.document.nodeType||!t.Element)return n.isSupported=!1,n;let{document:r}=t;const i=r,o=i.currentScript,{DocumentFragment:s,HTMLTemplateElement:l,Node:c,Element:u,NodeFilter:h,NamedNodeMap:p=t.NamedNodeMap||t.MozNamedAttrMap,HTMLFormElement:g,DOMParser:m,trustedTypes:j}=t,P=u.prototype,R=D(P,"cloneNode"),Y=D(P,"remove"),K=D(P,"nextSibling"),V=D(P,"childNodes"),Z=D(P,"parentNode");if("function"==typeof l){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let X,ee="";const{implementation:te,createNodeIterator:re,createDocumentFragment:se,getElementsByTagName:ae}=r,{importNode:le}=i;let ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};n.isSupported="function"==typeof a&&"function"==typeof Z&&te&&void 0!==te.createHTMLDocument;const{MUSTACHE_EXPR:ue,ERB_EXPR:he,TMPLIT_EXPR:de,DATA_ATTR:pe,ARIA_ATTR:fe,IS_SCRIPT_OR_DATA:ge,ATTR_WHITESPACE:me,CUSTOM_ELEMENT:ye}=ie;let{IS_ALLOWED_URI:_e}=ie,be=null;const ve=L({},[...N,...F,...U,...z,...W]);let we=null;const xe=L({},[...q,...B,...G,...J]);let Se=Object.seal(f(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ee=null,Ce=null;const Ae=Object.seal(f(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Oe=!0,Te=!0,ke=!1,Ie=!0,je=!1,Pe=!0,Le=!1,Re=!1,Me=!1,De=!1,Ne=!1,Fe=!1,Ue=!0,$e=!1,ze=!0,He=!1,We={},qe=null;const Be=L({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ge=null;const Je=L({},["audio","video","img","source","image","track"]);let Ye=null;const Ke=L({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",Ze="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml";let Qe=Xe,et=!1,tt=null;const nt=L({},[Ve,Ze,Xe],S);let rt=L({},["mi","mo","mn","ms","mtext"]),it=L({},["annotation-xml"]);const ot=L({},["title","style","font","a","script"]);let st=null;const at=["application/xhtml+xml","text/html"];let lt=null,ct=null;const ut=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},dt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ct||ct!==e){if(e&&"object"==typeof e||(e={}),e=M(e),st=-1===at.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,lt="application/xhtml+xml"===st?S:x,be=T(e,"ALLOWED_TAGS")?L({},e.ALLOWED_TAGS,lt):ve,we=T(e,"ALLOWED_ATTR")?L({},e.ALLOWED_ATTR,lt):xe,tt=T(e,"ALLOWED_NAMESPACES")?L({},e.ALLOWED_NAMESPACES,S):nt,Ye=T(e,"ADD_URI_SAFE_ATTR")?L(M(Ke),e.ADD_URI_SAFE_ATTR,lt):Ke,Ge=T(e,"ADD_DATA_URI_TAGS")?L(M(Je),e.ADD_DATA_URI_TAGS,lt):Je,qe=T(e,"FORBID_CONTENTS")?L({},e.FORBID_CONTENTS,lt):Be,Ee=T(e,"FORBID_TAGS")?L({},e.FORBID_TAGS,lt):M({}),Ce=T(e,"FORBID_ATTR")?L({},e.FORBID_ATTR,lt):M({}),We=!!T(e,"USE_PROFILES")&&e.USE_PROFILES,Oe=!1!==e.ALLOW_ARIA_ATTR,Te=!1!==e.ALLOW_DATA_ATTR,ke=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Ie=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,je=e.SAFE_FOR_TEMPLATES||!1,Pe=!1!==e.SAFE_FOR_XML,Le=e.WHOLE_DOCUMENT||!1,De=e.RETURN_DOM||!1,Ne=e.RETURN_DOM_FRAGMENT||!1,Fe=e.RETURN_TRUSTED_TYPE||!1,Me=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,$e=e.SANITIZE_NAMED_PROPS||!1,ze=!1!==e.KEEP_CONTENT,He=e.IN_PLACE||!1,_e=e.ALLOWED_URI_REGEXP||Q,Qe=e.NAMESPACE||Xe,rt=e.MATHML_TEXT_INTEGRATION_POINTS||rt,it=e.HTML_INTEGRATION_POINTS||it,Se=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Se.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Se.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Se.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),je&&(Te=!1),Ne&&(De=!0),We&&(be=L({},W),we=[],!0===We.html&&(L(be,N),L(we,q)),!0===We.svg&&(L(be,F),L(we,B),L(we,J)),!0===We.svgFilters&&(L(be,U),L(we,B),L(we,J)),!0===We.mathMl&&(L(be,z),L(we,G),L(we,J))),e.ADD_TAGS&&("function"==typeof e.ADD_TAGS?Ae.tagCheck=e.ADD_TAGS:(be===ve&&(be=M(be)),L(be,e.ADD_TAGS,lt))),e.ADD_ATTR&&("function"==typeof e.ADD_ATTR?Ae.attributeCheck=e.ADD_ATTR:(we===xe&&(we=M(we)),L(we,e.ADD_ATTR,lt))),e.ADD_URI_SAFE_ATTR&&L(Ye,e.ADD_URI_SAFE_ATTR,lt),e.FORBID_CONTENTS&&(qe===Be&&(qe=M(qe)),L(qe,e.FORBID_CONTENTS,lt)),e.ADD_FORBID_CONTENTS&&(qe===Be&&(qe=M(qe)),L(qe,e.ADD_FORBID_CONTENTS,lt)),ze&&(be["#text"]=!0),Le&&L(be,["html","head","body"]),be.table&&(L(be,["tbody"]),delete Ee.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw I('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw I('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');X=e.TRUSTED_TYPES_POLICY,ee=X.createHTML("")}else void 0===X&&(X=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const r="data-tt-policy-suffix";t&&t.hasAttribute(r)&&(n=t.getAttribute(r));const i="dompurify"+(n?"#"+n:"");try{return e.createPolicy(i,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+i+" could not be created."),null}}(j,o)),null!==X&&"string"==typeof ee&&(ee=X.createHTML(""));d&&d(e),ct=e}},pt=L({},[...F,...U,...$]),ft=L({},[...z,...H]),gt=function(e){v(n.removed,{element:e});try{Z(e).removeChild(e)}catch(t){Y(e)}},mt=function(e,t){try{v(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){v(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(De||Ne)try{gt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},yt=function(e){let t=null,n=null;if(Me)e=""+e;else{const t=E(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===st&&Qe===Xe&&(e=''+e+"");const i=X?X.createHTML(e):e;if(Qe===Xe)try{t=(new m).parseFromString(i,st)}catch(e){}if(!t||!t.documentElement){t=te.createDocument(Qe,"template",null);try{t.documentElement.innerHTML=et?ee:i}catch(e){}}const o=t.body||t.documentElement;return e&&n&&o.insertBefore(r.createTextNode(n),o.childNodes[0]||null),Qe===Xe?ae.call(t,Le?"html":"body")[0]:Le?t.documentElement:o},_t=function(e){return re.call(e.ownerDocument||e,e,h.SHOW_ELEMENT|h.SHOW_COMMENT|h.SHOW_TEXT|h.SHOW_PROCESSING_INSTRUCTION|h.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof g&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof p)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},vt=function(e){return"function"==typeof c&&e instanceof c};function wt(e,t,r){y(e,e=>{e.call(n,t,r,ct)})}const xt=function(e){let t=null;if(wt(ce.beforeSanitizeElements,e,null),bt(e))return gt(e),!0;const r=lt(e.nodeName);if(wt(ce.uponSanitizeElement,e,{tagName:r,allowedTags:be}),Pe&&e.hasChildNodes()&&!vt(e.firstElementChild)&&k(/<[/\w!]/g,e.innerHTML)&&k(/<[/\w!]/g,e.textContent))return gt(e),!0;if(7===e.nodeType)return gt(e),!0;if(Pe&&8===e.nodeType&&k(/<[/\w]/g,e.data))return gt(e),!0;if(!(Ae.tagCheck instanceof Function&&Ae.tagCheck(r))&&(!be[r]||Ee[r])){if(!Ee[r]&&Et(r)){if(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,r))return!1;if(Se.tagNameCheck instanceof Function&&Se.tagNameCheck(r))return!1}if(ze&&!qe[r]){const t=Z(e)||e.parentNode,n=V(e)||e.childNodes;if(n&&t)for(let r=n.length-1;r>=0;--r){const i=R(n[r],!0);i.__removalCount=(e.__removalCount||0)+1,t.insertBefore(i,K(e))}}return gt(e),!0}return e instanceof u&&!function(e){let t=Z(e);t&&t.tagName||(t={namespaceURI:Qe,tagName:"template"});const n=x(e.tagName),r=x(t.tagName);return!!tt[e.namespaceURI]&&(e.namespaceURI===Ze?t.namespaceURI===Xe?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||rt[r]):Boolean(pt[n]):e.namespaceURI===Ve?t.namespaceURI===Xe?"math"===n:t.namespaceURI===Ze?"math"===n&&it[r]:Boolean(ft[n]):e.namespaceURI===Xe?!(t.namespaceURI===Ze&&!it[r])&&!(t.namespaceURI===Ve&&!rt[r])&&!ft[n]&&(ot[n]||!pt[n]):!("application/xhtml+xml"!==st||!tt[e.namespaceURI]))}(e)?(gt(e),!0):"noscript"!==r&&"noembed"!==r&&"noframes"!==r||!k(/<\/no(script|embed|frames)/i,e.innerHTML)?(je&&3===e.nodeType&&(t=e.textContent,y([ue,he,de],e=>{t=C(t,e," ")}),e.textContent!==t&&(v(n.removed,{element:e.cloneNode()}),e.textContent=t)),wt(ce.afterSanitizeElements,e,null),!1):(gt(e),!0)},St=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in r||n in ut))return!1;if(Te&&!Ce[t]&&k(pe,t));else if(Oe&&k(fe,t));else if(Ae.attributeCheck instanceof Function&&Ae.attributeCheck(t,e));else if(!we[t]||Ce[t]){if(!(Et(e)&&(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,e)||Se.tagNameCheck instanceof Function&&Se.tagNameCheck(e))&&(Se.attributeNameCheck instanceof RegExp&&k(Se.attributeNameCheck,t)||Se.attributeNameCheck instanceof Function&&Se.attributeNameCheck(t,e))||"is"===t&&Se.allowCustomizedBuiltInElements&&(Se.tagNameCheck instanceof RegExp&&k(Se.tagNameCheck,n)||Se.tagNameCheck instanceof Function&&Se.tagNameCheck(n))))return!1}else if(Ye[t]);else if(k(_e,C(n,me,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==A(n,"data:")||!Ge[e])if(ke&&!k(ge,C(n,me,"")));else if(n)return!1;return!0},Et=function(e){return"annotation-xml"!==e&&E(e,ye)},Ct=function(e){wt(ce.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const r={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let i=t.length;for(;i--;){const o=t[i],{name:s,namespaceURI:a,value:l}=o,c=lt(s),u=l;let h="value"===s?u:O(u);if(r.attrName=c,r.attrValue=h,r.keepAttr=!0,r.forceKeepAttr=void 0,wt(ce.uponSanitizeAttribute,e,r),h=r.attrValue,!$e||"id"!==c&&"name"!==c||(mt(s,e),h="user-content-"+h),Pe&&k(/((--!?|])>)|<\/(style|title|textarea)/i,h)){mt(s,e);continue}if("attributename"===c&&E(h,"href")){mt(s,e);continue}if(r.forceKeepAttr)continue;if(!r.keepAttr){mt(s,e);continue}if(!Ie&&k(/\/>/i,h)){mt(s,e);continue}je&&y([ue,he,de],e=>{h=C(h,e," ")});const d=lt(e.nodeName);if(St(d,c,h)){if(X&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(a);else switch(j.getAttributeType(d,c)){case"TrustedHTML":h=X.createHTML(h);break;case"TrustedScriptURL":h=X.createScriptURL(h)}if(h!==u)try{a?e.setAttributeNS(a,s,h):e.setAttribute(s,h),bt(e)?gt(e):b(n.removed)}catch(t){mt(s,e)}}else mt(s,e)}wt(ce.afterSanitizeAttributes,e,null)},At=function e(t){let n=null;const r=_t(t);for(wt(ce.beforeSanitizeShadowDOM,t,null);n=r.nextNode();)wt(ce.uponSanitizeShadowNode,n,null),xt(n),Ct(n),n.content instanceof s&&e(n.content);wt(ce.afterSanitizeShadowDOM,t,null)};return n.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=null,o=null,a=null,l=null;if(et=!e,et&&(e="\x3c!--\x3e"),"string"!=typeof e&&!vt(e)){if("function"!=typeof e.toString)throw I("toString is not a function");if("string"!=typeof(e=e.toString()))throw I("dirty is not a string, aborting")}if(!n.isSupported)return e;if(Re||dt(t),n.removed=[],"string"==typeof e&&(He=!1),He){if(e.nodeName){const t=lt(e.nodeName);if(!be[t]||Ee[t])throw I("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)r=yt("\x3c!----\x3e"),o=r.ownerDocument.importNode(e,!0),1===o.nodeType&&"BODY"===o.nodeName||"HTML"===o.nodeName?r=o:r.appendChild(o);else{if(!De&&!je&&!Le&&-1===e.indexOf("<"))return X&&Fe?X.createHTML(e):e;if(r=yt(e),!r)return De?null:Fe?ee:""}r&&Me&>(r.firstChild);const u=_t(He?e:r);for(;a=u.nextNode();)xt(a),Ct(a),a.content instanceof s&&At(a.content);if(He)return e;if(De){if(Ne)for(l=se.call(r.ownerDocument);r.firstChild;)l.appendChild(r.firstChild);else l=r;return(we.shadowroot||we.shadowrootmode)&&(l=le.call(i,l,!0)),l}let h=Le?r.outerHTML:r.innerHTML;return Le&&be["!doctype"]&&r.ownerDocument&&r.ownerDocument.doctype&&r.ownerDocument.doctype.name&&k(ne,r.ownerDocument.doctype.name)&&(h="\n"+h),je&&y([ue,he,de],e=>{h=C(h,e," ")}),X&&Fe?X.createHTML(h):h},n.setConfig=function(){dt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Re=!0},n.clearConfig=function(){ct=null,Re=!1},n.isValidAttribute=function(e,t,n){ct||dt({});const r=lt(e),i=lt(t);return St(r,i,n)},n.addHook=function(e,t){"function"==typeof t&&v(ce[e],t)},n.removeHook=function(e,t){if(void 0!==t){const n=_(ce[e],t);return-1===n?void 0:w(ce[e],n,1)[0]}return b(ce[e])},n.removeHooks=function(e){ce[e]=[]},n.removeAllHooks=function(){ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},n}(),ae=n(744),le=n.n(ae),ce=function(){return ce=Object.assign||function(e){for(var t,n=1,r=arguments.length;nt})}class Ae extends(o()){_tag;_opts;_logs=[];constructor(e,t){super(),this._tag=e,this._opts=t}write(e,t,n){t?.length&&!0===t[t.length-1]&&(n=!0,t.pop());const r=t.reduce((e,t)=>(t&&t instanceof Error?e+=`${t.message} ${t.stack}`:e+=t.toString(),e),`[${this._tag}][${(new Date).toLocaleTimeString()}] `);this._logs.push([e,r]),(n||this._opts?.console)&&console?.["ERROR"===e?"error":"debug"](`${e}: ${r}`),this.emit("change")}clear(){this._logs=[],this.emit("change")}info(...e){this.write("INFO",e)}error(...e){this.write("ERROR",e)}warn(...e){this.write("WARN",e)}setTag(e){this._tag=e}toJSON(){return this._logs}}function Oe(e){return"string"==typeof e&&36===e.length&&/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(e)}function Te(){return"_"+Math.random().toString(36).substr(2,9)}function ke(e){return e.charAt(0).toUpperCase()+e.slice(1)}function Ie(e){return e?(/^(http|file|lsp)/.test(e)||(e=ye+e),e):""}function je(e,...t){try{const n=new URL(e);if(!n.origin)throw new Error(null);const r=ge.join(e.substr(n.origin.length),...t);return n.origin+r}catch(n){return ge.join(e,...t)}}function Pe(e){return e?.match(/^(http?|lsp|assets):/)||(e=ge.normalize(e)),e}function Le(e,t){let n,r,i=!1;const o=t=>n=>{e&&clearTimeout(e),t(n),i=!0},s=new Promise((i,s)=>{n=o(i),r=o(s),e&&(e=setTimeout(()=>r(new Error(`[deferred timeout] ${t}`)),e))});return{created:Date.now(),setTag:e=>t=e,resolve:n,reject:r,promise:s,get settled(){return i}}}function Re(t,...n){t=t?.startsWith("_call")?t:t?.replace(/^[_$]+/,"");let r=we(t);const i=window.logseq?.sdk,o=i&&Object.keys(i);let s={};const a=r?.split("_")?.[0];a&&o.includes(a)&&(r=r.replace(new RegExp(`^${a}_`),""),s=i?.[a]);const l=Object.assign({},window.logseq?.api,s,e),c=l[r]||window.apis[r]||l[t]||window.apis[t];if(!c)throw new Error(`Not existed method #${t}`);return"function"!=typeof c?c:c.apply(this,n)}function Me(e,t){const n=document.createElement("iframe");return n.classList.add("lsp-iframe-sandbox"),Object.entries(e).forEach(([e,t])=>{n.setAttribute(e,t)}),t.appendChild(n),async()=>{t.removeChild(n)}}function De(e,t){const n=t["data-injected-style"];let r=n&&document.querySelector(`[data-injected-style=${n}]`);if(!r)return r=document.createElement("style"),r.textContent=e,t&&Object.entries(t).forEach(([e,t])=>{r.setAttribute(e,t)}),document.head.append(r),()=>{document.head.removeChild(r)};r.textContent=e}const Ne=new Map;function Fe(e,t,n){let r,i,o="";const s=this;"slot"in e?(o=e.slot,r=`#${o}`):"path"in e?r=e.path:i=!0;const a=`${s.id}--${e.key||Te()}`,l=a,c=i?document.body:r&&document.querySelector(r);if(!c)return console.error(`${this.debugTag} can not resolve selector target ${r}`),!1;if(!e.template)return void Ne.get(a)?.call(null);e.template=se.sanitize(e.template,{ADD_TAGS:["iframe"],ALLOW_UNKNOWN_PROTOCOLS:!0,ADD_ATTR:["allow","src","allowfullscreen","frameborder","scrolling","target"]});let u,h,d=document.querySelector(`#${a}`),p=i?d?.querySelector(".ls-ui-float-content"):d;if(p){p.innerHTML=e.template,t&&Object.entries(t).forEach(([e,t])=>{d.setAttribute(e,t)});let n=null!=d.dataset.dx;return void(e.style&&Object.entries(e.style).forEach(([e,t])=>{n&&["left","top","bottom","right","width","height"].includes(e)||(d.style[e]=t)}))}if(d=document.createElement("div"),d.id=a,d.dataset.injectedUi=l||"",i?(p=document.createElement("div"),p.classList.add("ls-ui-float-content"),d.appendChild(p)):p=d,p.innerHTML=e.template,t&&Object.entries(t).forEach(([e,t])=>{d.setAttribute(e,t)}),e.style&&Object.entries(e.style).forEach(([e,t])=>{d.style[e]=t}),i&&(d.setAttribute("draggable","true"),d.setAttribute("resizable","true"),e.close&&(d.dataset.close=e.close),d.classList.add("lsp-ui-float-container","visible"),s._setupResizableContainer(d,l),h=s._setupDraggableContainer(d,{key:l,close:()=>u(),title:t?.title})),o&&e.reset){const e=Array.from(c.querySelectorAll("[data-injected-ui]")).map(e=>e.id);e?.forEach(e=>{Ne.get(e)?.call(null)})}return c.appendChild(d),["click","focus","focusin","focusout","blur","dblclick","keyup","keypress","keydown","change","input","contextmenu"].forEach(e=>{d.addEventListener(e,t=>{const n=t.target.closest(`[data-on-${e}]`);if(!n)return;const{preventDefault:r}=n.dataset,i=n.dataset[`on${ke(e)}`];i&&s.caller?.callUserModel(i,ze(n,t)),"true"===r?.toLowerCase()&&t.preventDefault()},!1)}),n?.({el:d,float:i}),u=()=>{h?.(),Ne.delete(a),c.removeChild(d)},Ne.set(a,u),u}function Ue(e){if(!Ne.has(e))return;const t=Ne.get(e);try{t()}catch(t){console.warn("[CLEAN Injected UI] ",e,t)}}function $e(){const e=document.head.querySelectorAll(`script[data-ref=${this.id}]`);e?.forEach(e=>e.remove())}function ze(e,t){const n={};if(e){n.type=t.type;const r=e.dataset,i="rect";["value","id","className","dataset",i].forEach(t=>{let o;if(t===i){if(!r.hasOwnProperty(i))return;o=e.getBoundingClientRect().toJSON()}else o=e[t];"object"==typeof o&&(o={...o}),n[t]=o})}return n}function He(e){const t=document.createElement("link");return t.rel="stylesheet",t.href=e,document.head.appendChild(t),()=>{try{document.head.removeChild(t)}catch(e){console.error(e)}}}function We(e,t){const n=(t||[]).reduce((e,t)=>("default"in t&&(e[t.key]=t.default),e),{});return Object.assign(n,e)}function qe(e){if("string"==typeof e)return e.trim().replace(/\s/g,"_").toLowerCase()}window.__injectedUIEffects=Ne;var Be=n(833),Ge=n.n(Be);const Je="application/x-postmate-v1+json";let Ye=0;const Ke={handshake:1,"handshake-reply":1,call:1,emit:1,reply:1,request:1},Ve=(e,t)=>!("string"==typeof t&&e.origin!==t||!e.data||"object"==typeof e.data&&!("postmate"in e.data)||e.data.type!==Je||!Ke[e.data.postmate]);class Ze{parent;frame;child;events={};childOrigin;listener;constructor(e){this.parent=e.parent,this.frame=e.frame,this.child=e.child,this.childOrigin=e.childOrigin,this.listener=e=>{if(!Ve(e,this.childOrigin))return!1;const{data:t,name:n}=((e||{}).data||{}).value||{};"emit"===e.data.postmate&&n in this.events&&this.events[n].forEach(e=>{e.call(this,t)})},this.parent.addEventListener("message",this.listener,!1)}get(e,...t){return new Promise((n,r)=>{const i=++Ye,o=e=>{e.data.uid===i&&"reply"===e.data.postmate&&(this.parent.removeEventListener("message",o,!1),e.data.error?r(e.data.error):n(e.data.value))};this.parent.addEventListener("message",o,!1),this.child.postMessage({postmate:"request",type:Je,property:e,args:t,uid:i},this.childOrigin)})}call(e,t){this.child.postMessage({postmate:"call",type:Je,property:e,data:t},this.childOrigin)}on(e,t){this.events[e]||(this.events[e]=[]),this.events[e].push(t)}destroy(){window.removeEventListener("message",this.listener,!1),this.frame.parentNode.removeChild(this.frame)}}class Xe{model;parent;parentOrigin;child;constructor(e){this.model=e.model,this.parent=e.parent,this.parentOrigin=e.parentOrigin,this.child=e.child,this.child.addEventListener("message",e=>{if(!Ve(e,this.parentOrigin))return;const{property:t,uid:n,data:r,args:i}=e.data;"call"!==e.data.postmate?((e,t,n)=>{const r="function"==typeof e[t]?e[t].apply(null,n):e[t];return Promise.resolve(r)})(this.model,t,i).then(r=>{e.source.postMessage({property:t,postmate:"reply",type:Je,uid:n,value:r},e.origin)}).catch(r=>{e.source.postMessage({property:t,postmate:"reply",type:Je,uid:n,error:r},e.origin)}):t in this.model&&"function"==typeof this.model[t]&&this.model[t](r)})}emit(e,t){this.parent.postMessage({postmate:"emit",type:Je,value:{name:e,data:t}},this.parentOrigin)}}class Qe{static debug=!1;container;parent;frame;child;childOrigin;url;model;static Model;constructor(e){this.container=e.container,this.url=e.url,this.parent=window,this.frame=document.createElement("iframe"),e.id&&(this.frame.id=e.id),e.name&&(this.frame.name=e.name),e.allow&&(this.frame.allow=e.allow),this.frame.classList.add.apply(this.frame.classList,e.classListArray||[]),this.container.appendChild(this.frame),this.child=this.frame.contentWindow,this.model=e.model||{}}sendHandshake(e){const t=(e=>{const t=document.createElement("a");t.href=e;const n=t.protocol.length>4?t.protocol:window.location.protocol,r=t.host.length?"80"===t.port||"443"===t.port?t.hostname:t.host:window.location.host;return t.origin||`${n}//${r}`})(e=e||this.url);let n,r=0;return new Promise((i,o)=>{const s=e=>!!Ve(e,t)&&("handshake-reply"===e.data.postmate?(clearInterval(n),this.parent.removeEventListener("message",s,!1),this.childOrigin=e.origin,i(new Ze(this))):o("Failed handshake"));this.parent.addEventListener("message",s,!1);const a=()=>{r++,this.child.postMessage({postmate:"handshake",type:Je,model:this.model},t),5===r&&clearInterval(n)};this.frame.addEventListener("load",()=>{a(),n=setInterval(a,500)}),this.frame.src=e})}destroy(){this.frame.parentNode.removeChild(this.frame)}}class et{child;model;parent;parentOrigin;constructor(e){this.child=window,this.model=e,this.parent=this.child.parent}sendHandshakeReply(){return new Promise((e,t)=>{const n=r=>{if(r.data.postmate){if("handshake"===r.data.postmate){this.child.removeEventListener("message",n,!1),r.source.postMessage({postmate:"handshake-reply",type:Je},r.origin),this.parentOrigin=r.origin;const t=r.data.model;return t&&Object.keys(t).forEach(e=>{this.model[e]=t[e]}),e(new Xe(this))}return t("Handshake Reply Failed")}};this.child.addEventListener("message",n,!1)})}}class tt{ctx;opts;constructor(e,t){this.ctx=e,this.opts=t}get ctxId(){return this.ctx.baseInfo.id}setItem(e,t){return this.ctx.caller.callAsync("api:call",{method:"write-plugin-storage-file",args:[this.ctxId,e,t,this.opts?.assets]})}getItem(e){return this.ctx.caller.callAsync("api:call",{method:"read-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}removeItem(e){return this.ctx.caller.call("api:call",{method:"unlink-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}allKeys(){return this.ctx.caller.callAsync("api:call",{method:"list-plugin-storage-files",args:[this.ctxId,this.opts?.assets]})}clear(){return this.ctx.caller.call("api:call",{method:"clear-plugin-storage-files",args:[this.ctxId,this.opts?.assets]})}hasItem(e){return this.ctx.caller.callAsync("api:call",{method:"exist-plugin-storage-file",args:[this.ctxId,e,this.opts?.assets]})}}class nt{ctx;constructor(e){this.ctx=e}get React(){return this.ensureHostScope().React}get ReactDOM(){return this.ensureHostScope().ReactDOM}get Components(){return{Editor:this.ensureHostScope().logseq.sdk.experiments.cp_page_editor}}get Utils(){const e=this.ensureHostScope().logseq.sdk.utils,t=t=>e[we(t)];return{toClj:t("toClj"),jsxToClj:t("jsxToClj"),toJs:t("toJs"),toKeyword:t("toKeyword"),toSymbol:t("toSymbol")}}get pluginLocal(){return this.ensureHostScope().LSPluginCore.ensurePlugin(this.ctx.baseInfo.id)}invokeExperMethod(e,...t){const n=this.ensureHostScope();e=we(e)?.toLowerCase();const r=n.logseq.api["exper_"+e]||n.logseq.sdk.experiments[e];return r?.apply(n,t)}async loadScripts(...e){(e=e.map(e=>e?.startsWith("http")?e:this.ctx.resolveResourceFullUrl(e))).unshift(this.ctx.baseInfo.id),await this.invokeExperMethod("loadScripts",...e)}registerFencedCodeRenderer(e,t){return this.invokeExperMethod("registerFencedCodeRenderer",this.ctx.baseInfo.id,e,t)}registerDaemonRenderer(e,t){return this.invokeExperMethod("registerDaemonRenderer",this.ctx.baseInfo.id,e,t)}registerRouteRenderer(e,t){return this.invokeExperMethod("registerRouteRenderer",this.ctx.baseInfo.id,e,t)}registerExtensionsEnhancer(e,t){const n=this.ensureHostScope();return"katex"===e&&n.katex&&t(n.katex).catch(console.error),this.invokeExperMethod("registerExtensionsEnhancer",this.ctx.baseInfo.id,e,t)}ensureHostScope(){try{window.top}catch(e){console.error("Can not access host scope!")}return window.top}}const rt=e=>`task_callback_${e}`;class it{_client;_requestId;_requestOptions;_promise;_aborted=!1;constructor(e,t,n={}){this._client=e,this._requestId=t,this._requestOptions=n,this._promise=new Promise((e,t)=>{if(!this._requestId)return t(null);this._client.once(rt(this._requestId),n=>{n&&n instanceof Error?t(n):e(n)})});const{success:r,fail:i,final:o}=this._requestOptions;this._promise.then(e=>{r?.(e)}).catch(e=>{i?.(e)}).finally(()=>{o?.()})}abort(){this._requestOptions.abortable&&!this._aborted&&(this._client.ctx._execCallableAPI("http_request_abort",this._requestId),this._aborted=!0)}get promise(){return this._promise}get client(){return this._client}get requestId(){return this._requestId}}class ot extends i.EventEmitter{_ctx;constructor(e){super(),this._ctx=e,this.ctx.caller.on("#lsp#request#callback",e=>{const t=e?.requestId;t&&this.emit(rt(t),e?.payload)})}static createRequestTask(e,t,n){return new it(e,t,n)}async _request(e){const t=this.ctx.baseInfo.id,{success:n,fail:r,final:i,...o}=e,s=this.ctx.Experiments.invokeExperMethod("request",t,o),a=ot.createRequestTask(this.ctx.Request,s,e);return o.abortable?a:a.promise}get ctx(){return this._ctx}}const st=Array.isArray,at="object"==typeof global&&global&&global.Object===Object&&global;var lt="object"==typeof self&&self&&self.Object===Object&&self;const ct=at||lt||Function("return this")(),ut=ct.Symbol;var ht=Object.prototype,dt=ht.hasOwnProperty,pt=ht.toString,ft=ut?ut.toStringTag:void 0;var gt=Object.prototype.toString;var mt=ut?ut.toStringTag:void 0;const yt=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":mt&&mt in Object(e)?function(e){var t=dt.call(e,ft),n=e[ft];try{e[ft]=void 0;var r=!0}catch(e){}var i=pt.call(e);return r&&(t?e[ft]=n:delete e[ft]),i}(e):function(e){return gt.call(e)}(e)},_t=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)},bt=function(e){if(!_t(e))return!1;var t=yt(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t},vt=ct["__core-js_shared__"];var wt,xt=(wt=/[^.]+$/.exec(vt&&vt.keys&&vt.keys.IE_PROTO||""))?"Symbol(src)_1."+wt:"";var St=Function.prototype.toString;const Et=function(e){if(null!=e){try{return St.call(e)}catch(e){}try{return e+""}catch(e){}}return""};var Ct=/^\[object .+?Constructor\]$/,At=Function.prototype,Ot=Object.prototype,Tt=At.toString,kt=Ot.hasOwnProperty,It=RegExp("^"+Tt.call(kt).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");const jt=function(e){return!(!_t(e)||(t=e,xt&&xt in t))&&(bt(e)?It:Ct).test(Et(e));var t},Pt=function(e,t){var n=function(e,t){return null==e?void 0:e[t]}(e,t);return jt(n)?n:void 0},Lt=function(){try{var e=Pt(Object,"defineProperty");return e({},"",{}),e}catch(e){}}(),Rt=function(e,t,n){for(var r=-1,i=Object(e),o=n(e),s=o.length;s--;){var a=o[++r];if(!1===t(i[a],a,i))break}return e},Mt=function(e){return null!=e&&"object"==typeof e},Dt=function(e){return Mt(e)&&"[object Arguments]"==yt(e)};var Nt=Object.prototype,Ft=Nt.hasOwnProperty,Ut=Nt.propertyIsEnumerable;const $t=Dt(function(){return arguments}())?Dt:function(e){return Mt(e)&&Ft.call(e,"callee")&&!Ut.call(e,"callee")};var zt="object"==typeof exports&&exports&&!exports.nodeType&&exports,Ht=zt&&"object"==typeof module&&module&&!module.nodeType&&module,Wt=Ht&&Ht.exports===zt?ct.Buffer:void 0;const qt=(Wt?Wt.isBuffer:void 0)||function(){return!1};var Bt=/^(?:0|[1-9]\d*)$/;const Gt=function(e,t){var n=typeof e;return!!(t=null==t?9007199254740991:t)&&("number"==n||"symbol"!=n&&Bt.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=9007199254740991};var Yt={};Yt["[object Float32Array]"]=Yt["[object Float64Array]"]=Yt["[object Int8Array]"]=Yt["[object Int16Array]"]=Yt["[object Int32Array]"]=Yt["[object Uint8Array]"]=Yt["[object Uint8ClampedArray]"]=Yt["[object Uint16Array]"]=Yt["[object Uint32Array]"]=!0,Yt["[object Arguments]"]=Yt["[object Array]"]=Yt["[object ArrayBuffer]"]=Yt["[object Boolean]"]=Yt["[object DataView]"]=Yt["[object Date]"]=Yt["[object Error]"]=Yt["[object Function]"]=Yt["[object Map]"]=Yt["[object Number]"]=Yt["[object Object]"]=Yt["[object RegExp]"]=Yt["[object Set]"]=Yt["[object String]"]=Yt["[object WeakMap]"]=!1;var Kt="object"==typeof exports&&exports&&!exports.nodeType&&exports,Vt=Kt&&"object"==typeof module&&module&&!module.nodeType&&module,Zt=Vt&&Vt.exports===Kt&&at.process,Xt=function(){try{return Vt&&Vt.require&&Vt.require("util").types||Zt&&Zt.binding&&Zt.binding("util")}catch(e){}}(),Qt=Xt&&Xt.isTypedArray;const en=Qt?(tn=Qt,function(e){return tn(e)}):function(e){return Mt(e)&&Jt(e.length)&&!!Yt[yt(e)]};var tn,nn=Object.prototype.hasOwnProperty;const rn=function(e,t){var n=st(e),r=!n&&$t(e),i=!n&&!r&&qt(e),o=!n&&!r&&!i&&en(e),s=n||r||i||o,a=s?function(e,t){for(var n=-1,r=Array(e);++n-1},pn.prototype.set=function(e,t){var n=this.__data__,r=hn(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this};const fn=pn,gn=Pt(ct,"Map"),mn=Pt(Object,"create");var yn=Object.prototype.hasOwnProperty;var _n=Object.prototype.hasOwnProperty;function bn(e){var t=-1,n=null==e?0:e.length;for(this.clear();++ta))return!1;var c=o.get(e),u=o.get(t);if(c&&u)return c==t&&u==e;var h=-1,d=!0,p=2&n?new On:void 0;for(o.set(e,t),o.set(t,e);++h(st(e?.blocks)&&(e.blocks=e.blocks.map(e=>e&&Pr(e,(e,t)=>`block/${t}`))),e)},rebuildBlocksIndice:{f:"onIndiceInit",args:["graph","blocks"]},transactBlocks:{f:"onBlocksChanged",args:["graph","data"]},truncateBlocks:{f:"onIndiceReset",args:["graph"]},removeDb:{f:"onGraph",args:["graph"]}}).forEach(([n,r])=>{const i=(e=>`service:search:${e}:${t.name}`)(n);e.caller.on(i,async n=>{if(bt(t?.[r.f])){let o=null;try{o=await t[r.f].apply(t,(r.args||[]).map(e=>{if(n){if(!0===e)return n;if(n.hasOwnProperty(e)){const t=n[e];return delete n[e],t}}})),r.transformOutput&&(o=r.transformOutput(o))}catch(e){console.error("[SearchService] ",e),o=e}finally{r.reply&&e.caller.call(`${i}:reply`,o)}}})})}}const Rr=Symbol.for("proxy-continue"),Mr=Ge()("LSPlugin:user"),Dr=new Ae("",{console:!0});function Nr(e,t,n){const{key:r,label:i,desc:o,palette:s,keybinding:a,extras:l}=t;if("function"!=typeof n)return this.logger.error(`${r||i}: command action should be function.`),!1;const c=qe(r);if(!c)return this.logger.error(`${i}: command key is required.`),!1;const u=`SimpleCommandHook${c}${++Hr}`;this.Editor["on"+u](n),this.caller?.call("api:call",{method:"register-plugin-simple-command",args:[this.baseInfo.id,[{key:c,label:i,type:e,desc:o,keybinding:a,extras:l},["editor/hook",u]],s]})}function Fr(e){return!!Oe(e)||(Dr.error(`#${e} is not a valid UUID string.`),!1)}let Ur=null,$r=new Map;const zr={async getInfo(e){return Ur||(Ur=await this._execCallableAPIAsync("get-app-info")),"string"==typeof e?Ur[e]:Ur},registerCommand:Nr,registerSearchService(e){if($r.has(e.name))throw new Error(`SearchService: #${e.name} has registered!`);$r.set(e.name,new Lr(this,e))},registerCommandPalette(e,t){const{key:n,label:r,keybinding:i}=e;return Nr.call(this,"$palette$",{key:n,label:r,palette:!0,keybinding:i},t)},registerCommandShortcut(e,t,n={}){"string"==typeof e&&(e={mode:"global",binding:e});const{binding:r}=e,i="$shortcut$",o=n.key||i+we(r?.toString());return Nr.call(this,i,{...n,key:o,palette:!1,keybinding:e},t)},registerUIItem(e,t){const n=this.baseInfo.id;this.caller?.call("api:call",{method:"register-plugin-ui-item",args:[n,e,t]})},registerPageMenuItem(e,t){if("function"!=typeof t)return!1;const n=e+"_"+this.baseInfo.id,r=e;Nr.call(this,"page-menu-item",{key:n,label:r},t)},onBlockRendererSlotted(e,t){if(!Fr(e))return;const n=this.baseInfo.id,r=`hook:editor:${we(`slot:${e}`)}`;return this.caller.on(r,t),this.App._installPluginHook(n,r),()=>{this.caller.off(r,t),this.App._uninstallPluginHook(n,r)}},invokeExternalPlugin(e,...t){if(e=e?.trim(),!e)return;let[n,r]=e.split(".");if(!["models","commands"].includes(r?.toLowerCase()))throw new Error("Type only support '.models' or '.commands' currently.");const i=e.replace(`${n}.${r}.`,"");if(!n||!r||!i)throw new Error(`Illegal type of #${e} to invoke external plugin.`);return this._execCallableAPIAsync("invoke_external_plugin_cmd",n,r.toLowerCase(),i,t)},setFullScreen(e){const t=(...e)=>this._callWin("setFullScreen",...e);"toggle"===e?this._callWin("isFullScreen").then(e=>{e?t():t(!0)}):e?t(!0):t()}};let Hr=0;const Wr={newBlockUUID(){return this._execCallableAPIAsync("new_block_uuid")},isPageBlock:e=>e.uuid&&e.hasOwnProperty("name"),registerSlashCommand(e,t){Mr("Register slash command #",this.baseInfo.id,e,t),"function"==typeof t&&(t=[["editor/clear-current-slash",!1],["editor/restore-saved-cursor"],["editor/hook",t]]),t=t.map(e=>{const[t,...n]=e;if("editor/hook"===t){let r=n[0],i=()=>{this.caller?.callUserModel(r)};"function"==typeof r&&(i=r);const o=`SlashCommandHook${t}${++Hr}`;e[1]=o,this.Editor["on"+o](i)}return e}),this.caller?.call("api:call",{method:"register-plugin-slash-command",args:[this.baseInfo.id,[e,t]]})},registerBlockContextMenuItem(e,t){if("function"!=typeof t)return!1;const n=e+"_"+this.baseInfo.id;Nr.call(this,"block-context-menu-item",{key:n,label:e},t)},registerHighlightContextMenuItem(e,t,n){if("function"!=typeof t)return!1;const r=e+"_"+this.baseInfo.id;Nr.call(this,"highlight-context-menu-item",{key:r,label:e,extras:n},t)},scrollToBlockInPage(e,t,n){const r="block-content-"+t;n?.replaceState?this.App.replaceState("page",{name:e},{anchor:r}):this.App.pushState("page",{name:e},{anchor:r})}},qr={onBlockChanged(e,t){if(!Fr(e))return;const n=this.baseInfo.id,r=`hook:db:${we(`block:${e}`)}`,i=({block:n,txData:r,txMeta:i})=>{n.uuid===e&&t(n,r,i)};return this.caller.on(r,i),this.App._installPluginHook(n,r),()=>{this.caller.off(r,i),this.App._uninstallPluginHook(n,r)}},datascriptQuery(e,...t){return t.pop(),t?.some(e=>"function"==typeof e)?this.Experiments.ensureHostScope().logseq.api.datascript_query(e,...t):this._execCallableAPIAsync("datascript_query",e,...t)}},Br={},Gr={},Jr={},Yr={makeSandboxStorage(){return new tt(this,{assets:!0})}};class Kr extends(o()){_baseInfo;_caller;_version=LIB_VERSION;_debugTag="";_settingsSchema;_connected=!1;_ui=new Map;_mFileStorage;_mRequest;_mExperiments;_beforeunloadCallback;constructor(e,t){super(),this._baseInfo=e,this._caller=t,t.on("sys:ui:visible",e=>{e?.toggle&&this.toggleMainUI()}),t.on("settings:changed",e=>{const t=Object.assign({},this.settings),n=Object.assign(this._baseInfo.settings,e);this.emit("settings:changed",{...n},t)}),t.on("beforeunload",async e=>{const{actor:t,...n}=e,r=this._beforeunloadCallback;try{r&&await r(n),t?.resolve(null)}catch(e){this.logger.error("[beforeunload] ",e),t?.reject(e)}})}async ready(e,t){if(!this._connected)try{"function"==typeof e&&(t=e,e={});let n=await this._caller.connectToParent(e);this._connected=!0,n=Ce(this._baseInfo,n),this._baseInfo=n,n?.id&&(this._debugTag=this._caller.debugTag=`#${n.id} [${n.name}]`,this.logger.setTag(this._debugTag)),this._settingsSchema&&(n.settings=We(n.settings,this._settingsSchema),await this.useSettingsSchema(this._settingsSchema));try{await this._execCallableAPIAsync("setSDKMetadata",{version:this._version,runtime:"js"})}catch(e){console.warn(e)}t&&t.call(this,n)}catch(e){console.error(`${this._debugTag} [Ready Error]`,e)}}ensureConnected(){if(!this._connected)throw new Error("not connected")}beforeunload(e){"function"==typeof e&&(this._beforeunloadCallback=e)}provideModel(e){return this.caller._extendUserModel(e),this}provideTheme(e){return this.caller.call("provider:theme",e),this}provideStyle(e){return this.caller.call("provider:style",e),this}provideUI(e){return this.caller.call("provider:ui",e),this}useSettingsSchema(e){return this.connected&&this.caller.call("settings:schema",{schema:e,isSync:!0}),this._settingsSchema=e,this}updateSettings(e){this.caller.call("settings:update",e)}onSettingsChanged(e){const t="settings:changed";return this.on(t,e),()=>this.off(t,e)}showSettingsUI(){this.caller.call("settings:visible:changed",{visible:!0})}hideSettingsUI(){this.caller.call("settings:visible:changed",{visible:!1})}setMainUIAttrs(e){this.caller.call("main-ui:attrs",e)}setMainUIInlineStyle(e){this.caller.call("main-ui:style",e)}hideMainUI(e){const t={key:0,visible:!1,cursor:e?.restoreEditingCursor};this.caller.call("main-ui:visible",t),this.emit("ui:visible:changed",t),this._ui.set(t.key,t)}showMainUI(e){const t={key:0,visible:!0,autoFocus:e?.autoFocus};this.caller.call("main-ui:visible",t),this.emit("ui:visible:changed",t),this._ui.set(t.key,t)}toggleMainUI(){const e=this._ui.get(0);e&&e.visible?this.hideMainUI():this.showMainUI()}get version(){return this._version}get isMainUIVisible(){const e=this._ui.get(0);return Boolean(e&&e.visible)}get connected(){return this._connected}get baseInfo(){return this._baseInfo}get effect(){return this&&(this.baseInfo?.effect||!this.baseInfo?.iir)}get logger(){return Dr}get settings(){return this.baseInfo?.settings}get caller(){return this._caller}resolveResourceFullUrl(e){if(this.ensureConnected(),e)return e=e.replace(/^[.\\/]+/,""),je(this._baseInfo.lsr,e)}_makeUserProxy(e,t){const n=this,r=this.caller;return new Proxy(e,{get(e,i,o){const s=e[i];return function(...e){if(s){0!==e?.length&&e.concat(t);const r=s.apply(n,e);if(r!==Rr)return r}if(t){const o=i.toString().match(/^(once|off|on)/i);if(null!=o){const i=o[0].toLowerCase(),s=o.input,a="off"===i,l=n.baseInfo.id;let c=s.slice(i.length),u=e[0],h=e[1];"string"==typeof u&&"function"==typeof h&&(u=u.replace(/^logseq./,":"),c=`${c}${u}`,u=h,h=e[2]),c=`hook:${t}:${we(c)}`,r[i](c,u);const d=()=>{r.off(c,u),r.listenerCount(c)||n.App._uninstallPluginHook(l,c)};return a?void d():(n.App._installPluginHook(l,c,h),d)}}let o=i;return["git","ui","assets","utils"].includes(t)&&(o=t+"_"+o),r.callAsync("api:call",{tag:t,method:o,args:e})}}})}_execCallableAPIAsync(e,...t){return this._caller.callAsync("api:call",{method:e,args:t})}_execCallableAPI(e,...t){this._caller.call("api:call",{method:e,args:t})}_callWin(...e){return this._execCallableAPIAsync("_callMainWin",...e)}#e;#t;#n;#r;#i;get App(){return this.#e?this.#e:this.#e=this._makeUserProxy(zr,"app")}get Editor(){return this.#t?this.#t:this.#t=this._makeUserProxy(Wr,"editor")}get DB(){return this.#n?this.#n:this.#n=this._makeUserProxy(qr,"db")}get UI(){return this.#r?this.#r:this.#r=this._makeUserProxy(Gr,"ui")}get Utils(){return this.#i?this.#i:this.#i=this._makeUserProxy(Jr,"utils")}get Git(){return this._makeUserProxy(Br,"git")}get Assets(){return this._makeUserProxy(Yr,"assets")}get FileStorage(){let e=this._mFileStorage;return e||(e=this._mFileStorage=new tt(this)),e}get Request(){let e=this._mRequest;return e||(e=this._mRequest=new ot(this)),e}get Experiments(){let e=this._mExperiments;return e||(e=this._mExperiments=new nt(this)),e}}if(null==window.__LSP__HOST__){const e=new ui(null);window.logseq=new Kr({},e)}const{importHTML:Vr,createSandboxContainer:Zr}=window.QSandbox||{};function Xr(e,t){return e.startsWith("http")?fetch(e,t):(e=e.replace("file://",""),new Promise(async(t,n)=>{try{const n=await window.apis.doAction(["readFile",e]);t({text:()=>n})}catch(e){console.error(e),n(e)}}))}class Qr extends(o()){_pluginLocal;_frame;_root;_loaded=!1;_unmountFns=[];constructor(e){super(),this._pluginLocal=e,e._dispose(()=>{this._unmount()})}async load(){const{name:e,entry:t}=this._pluginLocal.options;if(this.loaded||!t)return;const{template:n,execScripts:r}=await Vr(t,{fetch:Xr});this._mount(n,document.body);const i=Zr(e,{elementGetter:()=>this._root?.firstChild}).instance.proxy;i.__shadow_mode__=!0,i.LSPluginLocal=this._pluginLocal,i.LSPluginShadow=this,i.LSPluginUser=i.logseq=new Kr(this._pluginLocal.toJSON(),this._pluginLocal.caller);const o=await r(i,!0);this._unmountFns.push(o.unmount),this._loaded=!0}_mount(e,t){const n=this._frame=document.createElement("div");n.classList.add("lsp-shadow-sandbox"),n.id=this._pluginLocal.id,this._root=n.attachShadow({mode:"open"}),this._root.innerHTML=`${e}
`,t.appendChild(n),this.emit("mounted")}_unmount(){for(const e of this._unmountFns)e&&e.call(null)}destroy(){this.frame?.parentNode?.removeChild(this.frame)}get loaded(){return this._loaded}get document(){return this._root?.firstChild}get frame(){return this._frame}}const ei=Ge()("LSPlugin:caller"),ti="#await#response#",ni="#lspmsg#",ri="#lspmsg#error#",ii="#lspmsg#settings#",oi="#lspmsg#beforeunload#",si="#lspmsg#reply#",ai="#lspmsg#ready#",li=e=>`${ni}${e}`,ci=e=>`${ti}${e}`;class ui extends(o()){_pluginLocal;_connected=!1;_parent;_child;_shadow;_status;_userModel={};_call;_callUserModel;_debugTag="";constructor(e){super(),this._pluginLocal=e,e&&(this._debugTag=e.debugTag)}async connectToChild(){if(this._connected)return;const{shadow:e}=this._pluginLocal;e?await this._setupShadowSandbox():await this._setupIframeSandbox()}async connectToParent(e={}){if(this._connected)return;const t=this,n=null!=this._pluginLocal;let r=0,i=0;const o=new Map,s=Le(6e4),a=this._extendUserModel({[ai]:async e=>{a[li(e?.pid)]=({type:e,payload:n})=>{ei(`[host (_call) -> *user] ${this._debugTag}`,e,n),t.emit(e,n)},await s.resolve()},[oi]:async e=>{const n=Le(1e4);t.emit("beforeunload",Object.assign({actor:n},e)),await n.promise},[ii]:async({type:e,payload:n})=>{t.emit("settings:changed",n)},[ni]:async({ns:e,type:n,payload:r})=>{ei(`[host (async) -> *user] ${this._debugTag} ns=${e} type=${n}`,r),e&&e.startsWith("hook")?t.emit(`${e}:${n}`,r):t.emit(n,r)},[si]:({_sync:e,result:t})=>{if(ei(`[sync host -> *user] #${e}`,t),o.has(e)){const n=o.get(e);n&&(t?.hasOwnProperty(ri)?n.reject(t[ri]):n.resolve(t),o.delete(e))}},...e});if(n)return await s.promise,JSON.parse(JSON.stringify(this._pluginLocal?.toJSON()));const l=new et(a).sendHandshakeReply();return this._status="pending",await l.then(e=>{this._child=e,this._connected=!0,this._call=async(t,n={},r)=>{if(r){const e=++i;o.set(e,r),n._sync=e,r.setTag(`async call #${e}`),ei(`async call #${e}`)}return e.emit(li(a.baseInfo.id),{type:t,payload:n}),r?.promise},this._callUserModel=async(e,t)=>{try{a[e](t)}catch(t){ei(`[model method] #${e} not existed`)}},r=setInterval(()=>{if(o.size>100)for(const[e,t]of o)t.settled&&o.delete(e)},18e5)}).finally(()=>{this._status=void 0}),await s.promise,a.baseInfo}async call(e,t={}){return this._call?.call(this,e,t)}async callAsync(e,t={}){const n=Le(1e4);return this._call?.call(this,e,t,n)}async callUserModel(e,...t){return this._callUserModel?.apply(this,[e,...t])}async callUserModelAsync(e,...t){return e=ci(e),this._callUserModel?.apply(this,[e,...t])}async _setupIframeSandbox(){const e=this._pluginLocal,t=e.id,n=`${t}_lsp_main`,r=new URL(e.options.entry);r.searchParams.set("__v__",me?Date.now().toString():e.options.version);const i=document.querySelector(`#${n}`);i&&i.parentElement.removeChild(i);const o=document.createElement("div");o.classList.add("lsp-iframe-sandbox-container"),o.id=n,o.dataset.pid=t;try{const e=(await this._pluginLocal._loadLayoutsData())?.$$0;if(e){o.dataset.inited_layout="true";let{width:t,height:n,left:r,top:i,vw:s,vh:a}=e;r=Math.max(r,0),r="number"==typeof s?`${Math.min(100*r/s,99)}%`:`${r}px`,i=Math.max(i,45),i="number"==typeof a?`${Math.min(100*i/a,99)}%`:`${i}px`,Object.assign(o.style,{width:t+"px",height:n+"px",left:r,top:i})}}catch(e){console.error("[Restore Layout Error]",e)}document.body.appendChild(o);const s=new Qe({id:t+"_iframe",container:o,url:r.href,classListArray:["lsp-iframe-sandbox"],model:{baseInfo:JSON.parse(JSON.stringify(e.toJSON()))},allow:e.options.allow});let a,l=s.sendHandshake();return this._status="pending",new Promise((t,n)=>{a=setTimeout(()=>{n(new Error("handshake Timeout")),s.destroy()},8e3),l.then(n=>{this._parent=n,this._connected=!0,this.emit("connected"),n.on(li(e.id),({type:e,payload:t})=>{ei("[user -> *host] ",e,t),this._pluginLocal?.emit(e,t||{}),this._pluginLocal?.caller.emit(e,t||{})}),this._call=async(...t)=>{n.call(li(e.id),{type:t[0],payload:Object.assign(t[1]||{},{$$pid:e.id})})},this._callUserModel=async(e,...t)=>{if(e.startsWith(ti))return await n.get(e.replace(ti,""),...t);n.call(e,t?.[0])},t(null)}).catch(e=>{n(e)}).finally(()=>{clearTimeout(a)})}).catch(e=>{throw ei("[iframe sandbox] error",e),e}).finally(()=>{this._status=void 0})}async _setupShadowSandbox(){const e=this._pluginLocal,t=this._shadow=new Qr(e);try{this._status="pending",await t.load(),this._connected=!0,this.emit("connected"),this._call=async(t,n={},r)=>(r&&(n.actor=r),this._pluginLocal?.emit(t,Object.assign(n,{$$pid:e.id})),r?.promise),this._callUserModel=async(...e)=>{let t=e[0];t?.startsWith(ti)&&(t=t.replace(ti,""));const n=e[1]||{},r=this._userModel[t];"function"==typeof r&&await r.call(null,n)}}catch(e){throw ei("[shadow sandbox] error",e),e}finally{this._status=void 0}}_extendUserModel(e){return Object.assign(this._userModel,e)}_getSandboxIframeContainer(){return this._parent?.frame.parentNode}_getSandboxShadowContainer(){return this._shadow?.frame.parentNode}_getSandboxIframeRoot(){return this._parent?.frame}_getSandboxShadowRoot(){return this._shadow?.frame}set debugTag(e){this._debugTag=e}async destroy(){let e=null;this._parent&&(e=this._getSandboxIframeContainer(),await this._parent.destroy()),this._shadow&&(e=this._getSandboxShadowContainer(),this._shadow.destroy()),e?.parentNode.removeChild(e)}}const hi=Ge()("LSPlugin:core"),di="plugins";class pi extends(o()){_userPluginSettings;_schema;_settings={disabled:!1};constructor(e,t){super(),this._userPluginSettings=e,this._schema=t,Object.assign(this._settings,e)}get(e){return this._settings[e]}set(e,t){const n=Ce({},this._settings);if("string"==typeof e){if(this._settings[e]==t)return;this._settings[e]=t}else{if(!Ee(e))return;this._settings=Ce(this._settings,e)}this.emit("change",{...this._settings},n)}set settings(e){const t=Ce({},this._settings);this._settings=e||{},this.emit("change",{...this._settings},t)}get settings(){return this._settings}setSchema(e,t){if(this._schema=e,t){const t=this._settings;this._settings=We(t,e),this.emit("change",this._settings,t)}}reset(){const e=this.settings,t={};this._schema,this.settings=t,this.emit("reset",t,e)}toJSON(){return this._settings}}var fi;function gi(e,t){return t&&e.startsWith(ye+t)&&(e=je(be,e.substr(ye.length+t.length))),e}!function(e){e.LOADING="loading",e.UNLOADING="unloading",e.LOADED="loaded",e.UNLOADED="unload",e.ERROR="error"}(fi||(fi={}));class mi extends Error{constructor(e){super(e),this.name="IllegalPluginPackageError"}}class yi extends Error{constructor(e){super(e),this.name="ExistedImportedPluginPackageError"}}class _i extends(o()){_options;_themeMgr;_ctx;_sdk={};_disposes=[];_id;_status=fi.UNLOADED;_loadErr;_localRoot;_dotSettingsFile;_caller;_logger=new Ae("PluginLocal");constructor(e,t,n){var r;super(),this._options=e,this._themeMgr=t,this._ctx=n,this._id=e.key||Te(),function(e){const t=e=>`settings:${e}`;e.on(t("schema"),({schema:t,isSync:n})=>{e.settingsSchema=t,e.settings?.setSchema(t,n)}),e.on(t("update"),t=>{t&&e.settings?.set(t)}),e.on(t("visible:changed"),t=>{const n=t?.visible;Re("set_focused_settings",n?e.id:null)})}(this),function(e){const t=e=>`main-ui:${e}`;e.on(t("visible"),({visible:t,toggle:n,cursor:r,autoFocus:i})=>{const o=e.getMainUIContainer();o?.classList[n?"toggle":t?"add":"remove"]("visible"),t?!e.shadow&&o&&!1!==i&&o.querySelector("iframe")?.contentWindow?.focus():o.ownerDocument.activeElement.blur(),r&&Re("restore_editing_cursor")}),e.on(t("attrs"),t=>{const n=e.getMainUIContainer();Object.entries(t).forEach(([t,r])=>{n?.setAttribute(t,String(r)),"draggable"===t&&r&&e._dispose(e._setupDraggableContainer(n,{title:e.options.name,close:()=>{e.caller.call("sys:ui:visible",{toggle:!0})}})),"resizable"===t&&r&&e._dispose(e._setupResizableContainer(n))})}),e.on(t("style"),t=>{const n=e.getMainUIContainer(),r=!!n.dataset.inited_layout;Object.entries(t).forEach(([e,t])=>{r&&["left","top","bottom","right","width","height"].includes(e)||(n.style[e]=t)})})}(this),function(e){const t=e=>`provider:${e}`;let n=!1;e.on(t("theme"),t=>{e.themeMgr.registerTheme(e.id,t),n||(e._dispose(()=>{e.themeMgr.unregisterTheme(e.id)}),n=!0)}),e.on(t("style"),t=>{let n;"string"!=typeof t&&(n=t.key,t=t.style),t&&t.trim()&&e._dispose(De(t,{"data-injected-style":n?`${n}-${e.id}`:"","data-ref":e.id}))}),e.on(t("ui"),t=>{e._onHostMounted(()=>{const n=Fe.call(e,t,Object.assign({"data-ref":e.id},t.attrs||{}),({el:t,float:n})=>{if(!n)return;const r=t.dataset.identity;e.layoutCore.move_container_to_top(r)});"function"==typeof n&&e._dispose(n)})})}(this),(r=this).on("api:call",async e=>{let t;try{window.$$callerPluginID=r.id,t=await Re.apply(r,[e.method,...e.args])}catch(e){t={[ri]:e}}finally{window.$$callerPluginID=void 0}if(r.shadow)return void(e.actor&&e.actor.resolve(t));const{_sync:n}=e;if(null!=n){const e=e=>{r.caller?.callUserModel(si,{result:e,_sync:n})};Promise.resolve(t).then(e,e)}})}async _setupUserSettings(e){const{_options:t}=this,n=this._logger=new Ae(`Loader:${this.debugTag}`);if(!t.settings||e)try{const n=()=>Re("load_plugin_user_settings",this.id),[r,i]=await n();this._dotSettingsFile=r;let o=t.settings;if(o||(o=t.settings=new pi(i)),e)return void(o.settings=i);const s=async(e,t)=>{if(hi("Settings changed",this.debugTag,e),!e.disabled&&t.disabled){const[,t]=await n();t.disabled=!1,e=Object.assign(e,t),o.settings=e,await this.load()}if(e.disabled&&!t.disabled){const[,t]=await n();t.disabled=!0,e=Object.assign(e,t),await this.unload()}e&&Re("save_plugin_user_settings",this.id,e)};return o.on("change",s),()=>{}}catch(e){hi("[load plugin user settings Error]",e),n?.error(e)}}getMainUIContainer(){return this.shadow?this.caller?._getSandboxShadowContainer():this.caller?._getSandboxIframeContainer()}_resolveResourceFullUrl(e,t){if(!e?.trim())return;if(t=t||this._localRoot,this.isWebPlugin)return this.installedFromUserWebUrl?`${this.installedFromUserWebUrl}/${e}`:`https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${ge.join(t,e)}`;const n=/^(http|file)/;if(!n.test(e)){const r=ge.join(t,e);e=n.test(r)?r:ye+r}return!this.options.effect&&this.isInstalledInLocalDotRoot?gi(e,this.dotPluginsRoot):e}async _preparePackageConfigs(){const{url:e,webPkg:t}=this._options;let n=t;if(!n)try{if(!e)throw new Error("Can not resolve package config location");if(hi("prepare package root",e),n=await Re("load_plugin_config",e),!n||(n=JSON.parse(n),!n))throw new Error(`Parse package config error #${e}/package.json`)}catch(e){throw new mi(e.message)}["name","author","repository","version","description","repo","title","effect","sponsors"].concat(this.isInstalledInLocalDotRoot?[]:["devEntry"]).forEach(e=>{this._options[e]=n[e]});const{repo:r,version:i}=this._options,o=this._localRoot=this.isWebPlugin?`${r||e}/${i}`:Pe(e),s=n.logseq||{},a=s.entry||s.main||n.main;s.devEntry?(this._options.devEntry=s.devEntry,this._options.entry=s.devEntry):this._options.entry=this._resolveResourceFullUrl(a,o),s.mode&&(this._options.mode=s.mode);const l=s.title||n.title,c=s.icon||n.icon;if(this._options.title=l,this._options.icon=c&&this._resolveResourceFullUrl(c),this._options.theme=Boolean(s.theme||!!s.themes),this.isInstalledInLocalDotRoot)this._id=ge.basename(o);else if(!this.isWebPlugin)if(s.id)this._id=s.id;else{s.id=this.id;try{await Re("save_plugin_package_json",e,{...n,logseq:s})}catch(e){hi("[save plugin ID Error] ",e)}}const{registeredPlugins:u,isRegistering:h}=this._ctx;if(h&&u.has(this.id))throw new yi(this.id);return async()=>{try{const e=s.themes;e&&await this._loadConfigThemes(Array.isArray(e)?e:[e])}catch(e){hi("[prepare package effect Error]",e)}}}async _tryToNormalizeEntry(){let{entry:e,settings:t,devEntry:n}=this.options;if(n=n||t?.get("_devEntry"),n)return void(this._options.entry=n);if(!e.endsWith(".js"))return;let r=null,i="write_user_tmp_file";this.isInstalledInLocalDotRoot&&(i="write_dotdir_file",r=this._localRoot.replace(this.dotPluginsRoot,""),r=ge.join(di,r));const o=(new Date).getDay(),s=await Se(),a=await Re(i,`${this._id}_index.html`,`\n\n \n \n logseq plugin entry\n ${me?`